fix(runtime): gate codex install prompt on runtime status
This commit is contained in:
parent
8f4a4dd502
commit
53dec55b1d
6 changed files with 280 additions and 22 deletions
|
|
@ -24,6 +24,10 @@ import {
|
|||
CodexLoginLinkCopyButton,
|
||||
CodexLoginUserCodeBadge,
|
||||
} from '@renderer/components/runtime/CodexLoginLinkCopyButton';
|
||||
import {
|
||||
isCodexProviderRuntimeMissing,
|
||||
shouldOfferCodexRuntimeInstall,
|
||||
} from '@renderer/components/runtime/codexRuntimeInstallAction';
|
||||
import {
|
||||
formatProviderStatusText,
|
||||
getProviderConnectionModeSummary,
|
||||
|
|
@ -606,30 +610,12 @@ function shouldShowCodexInstallAction(
|
|||
showSkeleton: boolean,
|
||||
codexRuntimeStatus: CodexRuntimeStatus | null
|
||||
): boolean {
|
||||
const codexNativeBackend = provider.availableBackends?.find(
|
||||
(backend) => backend.id === 'codex-native'
|
||||
);
|
||||
const runtimeMissingText = [
|
||||
provider.statusMessage,
|
||||
provider.detailMessage,
|
||||
codexNativeBackend?.statusMessage,
|
||||
codexNativeBackend?.detailMessage,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
const runtimeMissing =
|
||||
provider.verificationState === 'error' &&
|
||||
(codexNativeBackend?.state === 'runtime-missing' ||
|
||||
runtimeMissingText.includes('codex cli not found') ||
|
||||
runtimeMissingText.includes('runtime missing'));
|
||||
|
||||
return (
|
||||
provider.providerId === 'codex' &&
|
||||
!showSkeleton &&
|
||||
!provider.authenticated &&
|
||||
runtimeMissing &&
|
||||
!(codexRuntimeStatus?.source === 'app-managed' && codexRuntimeStatus.state !== 'failed')
|
||||
isCodexProviderRuntimeMissing(provider) &&
|
||||
shouldOfferCodexRuntimeInstall(codexRuntimeStatus)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1368,6 +1354,7 @@ export const CliStatusBanner = (): React.JSX.Element | null => {
|
|||
bootstrapCliStatus,
|
||||
fetchCliStatus,
|
||||
fetchCliProviderStatus,
|
||||
fetchCodexRuntimeStatus,
|
||||
invalidateCliStatus,
|
||||
installCli,
|
||||
installOpenCodeRuntime,
|
||||
|
|
@ -1455,6 +1442,23 @@ export const CliStatusBanner = (): React.JSX.Element | null => {
|
|||
[loadingCliStatus, visibleCliProviders]
|
||||
);
|
||||
const renderCliStatus = effectiveCliStatus;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isElectron || codexRuntimeStatus || codexRuntimeStatusLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (visibleCliProviders.some(isCodexProviderRuntimeMissing)) {
|
||||
void fetchCodexRuntimeStatus();
|
||||
}
|
||||
}, [
|
||||
codexRuntimeStatus,
|
||||
codexRuntimeStatusLoading,
|
||||
fetchCodexRuntimeStatus,
|
||||
isElectron,
|
||||
visibleCliProviders,
|
||||
]);
|
||||
|
||||
const shouldPollAnthropicSubscriptionLimits = useMemo(() => {
|
||||
if (
|
||||
!renderCliStatus?.installed ||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ import { Tabs, TabsList, TabsTrigger } from '@renderer/components/ui/tabs';
|
|||
import { useStore } from '@renderer/store';
|
||||
import { AlertTriangle, Download, Key, Link2, Loader2, Save, Trash2 } from 'lucide-react';
|
||||
|
||||
import {
|
||||
isCodexProviderRuntimeMissing,
|
||||
shouldOfferCodexRuntimeInstall,
|
||||
} from './codexRuntimeInstallAction';
|
||||
import {
|
||||
formatProviderAuthMethodLabelForProvider,
|
||||
formatProviderAuthModeLabelForProvider,
|
||||
|
|
@ -1056,8 +1060,8 @@ export const ProviderRuntimeSettingsDialog = ({
|
|||
const showCodexRuntimeInstallAction =
|
||||
selectedProvider?.providerId === 'codex' &&
|
||||
typeof onInstallCodexRuntime === 'function' &&
|
||||
codexConnection?.appServerState === 'runtime-missing' &&
|
||||
!(codexRuntimeStatus?.source === 'app-managed' && codexRuntimeStatus.state !== 'failed');
|
||||
isCodexProviderRuntimeMissing(selectedProvider) &&
|
||||
shouldOfferCodexRuntimeInstall(codexRuntimeStatus);
|
||||
const runtimeBusy = disabled || selectedProviderLoading || runtimeSaving;
|
||||
const anthropicFastModeCapability =
|
||||
selectedProvider?.providerId === 'anthropic'
|
||||
|
|
|
|||
47
src/renderer/components/runtime/codexRuntimeInstallAction.ts
Normal file
47
src/renderer/components/runtime/codexRuntimeInstallAction.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import type { CodexRuntimeStatus } from '@features/codex-runtime-installer/contracts';
|
||||
import type { CliProviderStatus } from '@shared/types';
|
||||
|
||||
const CODEX_NATIVE_BACKEND_ID = 'codex-native';
|
||||
|
||||
export function isCodexProviderRuntimeMissing(provider: CliProviderStatus): boolean {
|
||||
if (provider.providerId !== 'codex') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const codexNativeBackend = provider.availableBackends?.find(
|
||||
(backend) => backend.id === CODEX_NATIVE_BACKEND_ID
|
||||
);
|
||||
const runtimeMissingText = [
|
||||
provider.statusMessage,
|
||||
provider.detailMessage,
|
||||
codexNativeBackend?.statusMessage,
|
||||
codexNativeBackend?.detailMessage,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
|
||||
return (
|
||||
provider.connection?.codex?.appServerState === 'runtime-missing' ||
|
||||
codexNativeBackend?.state === 'runtime-missing' ||
|
||||
(provider.verificationState === 'error' &&
|
||||
(runtimeMissingText.includes('codex cli not found') ||
|
||||
runtimeMissingText.includes('runtime missing')))
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldOfferCodexRuntimeInstall(
|
||||
codexRuntimeStatus: CodexRuntimeStatus | null | undefined
|
||||
): boolean {
|
||||
if (!codexRuntimeStatus || codexRuntimeStatus.installed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
codexRuntimeStatus.source === 'missing' ||
|
||||
codexRuntimeStatus.state === 'failed' ||
|
||||
codexRuntimeStatus.state === 'checking' ||
|
||||
codexRuntimeStatus.state === 'downloading' ||
|
||||
codexRuntimeStatus.state === 'installing'
|
||||
);
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import { useAppTranslation } from '@features/localization/renderer';
|
|||
import { isElectronMode } from '@renderer/api';
|
||||
import { confirm } from '@renderer/components/common/ConfirmDialog';
|
||||
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
||||
import { isCodexProviderRuntimeMissing } from '@renderer/components/runtime/codexRuntimeInstallAction';
|
||||
import {
|
||||
formatProviderStatusText,
|
||||
getProviderConnectionModeSummary,
|
||||
|
|
@ -148,6 +149,7 @@ export const CliStatusSection = (): React.JSX.Element | null => {
|
|||
bootstrapCliStatus,
|
||||
fetchCliStatus,
|
||||
fetchCliProviderStatus,
|
||||
fetchCodexRuntimeStatus,
|
||||
installCodexRuntime,
|
||||
installCli,
|
||||
isBusy,
|
||||
|
|
@ -226,6 +228,22 @@ export const CliStatusSection = (): React.JSX.Element | null => {
|
|||
}
|
||||
}, [bootstrapCliStatus, cliStatus, fetchCliStatus, isElectron, multimodelEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isElectron || codexRuntimeStatus || codexRuntimeStatusLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (visibleEffectiveProviders.some(isCodexProviderRuntimeMissing)) {
|
||||
void fetchCodexRuntimeStatus();
|
||||
}
|
||||
}, [
|
||||
codexRuntimeStatus,
|
||||
codexRuntimeStatusLoading,
|
||||
fetchCodexRuntimeStatus,
|
||||
isElectron,
|
||||
visibleEffectiveProviders,
|
||||
]);
|
||||
|
||||
const handleInstall = useCallback(() => {
|
||||
installCli();
|
||||
}, [installCli]);
|
||||
|
|
|
|||
|
|
@ -1755,6 +1755,110 @@ describe('ProviderRuntimeSettingsDialog', () => {
|
|||
expect(icon?.className).toContain('shrink-0');
|
||||
});
|
||||
|
||||
it('does not offer Codex runtime install before installer status is known', async () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
const onInstallCodexRuntime = vi.fn();
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(ProviderRuntimeSettingsDialog, {
|
||||
open: true,
|
||||
onOpenChange: vi.fn(),
|
||||
providers: [
|
||||
createCodexProvider({
|
||||
authenticated: false,
|
||||
authMethod: null,
|
||||
codex: {
|
||||
appServerState: 'runtime-missing',
|
||||
appServerStatusMessage: 'Codex CLI not found',
|
||||
launchAllowed: false,
|
||||
launchIssueMessage: 'Codex CLI not found',
|
||||
launchReadinessState: 'runtime_missing',
|
||||
},
|
||||
availableBackends: [
|
||||
{
|
||||
id: 'codex-native',
|
||||
label: 'Codex native',
|
||||
description: 'Use the local codex exec JSON seam.',
|
||||
selectable: false,
|
||||
recommended: true,
|
||||
available: false,
|
||||
state: 'runtime-missing',
|
||||
audience: 'general',
|
||||
statusMessage: 'Codex CLI not found',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
initialProviderId: 'codex',
|
||||
codexRuntimeStatus: null,
|
||||
onInstallCodexRuntime,
|
||||
onSelectBackend: vi.fn(),
|
||||
onRefreshProvider: vi.fn(() => Promise.resolve(undefined)),
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.textContent).not.toContain('Install Codex CLI');
|
||||
expect(onInstallCodexRuntime).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('offers Codex runtime install after installer status confirms the runtime is missing', async () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(ProviderRuntimeSettingsDialog, {
|
||||
open: true,
|
||||
onOpenChange: vi.fn(),
|
||||
providers: [
|
||||
createCodexProvider({
|
||||
authenticated: false,
|
||||
authMethod: null,
|
||||
codex: {
|
||||
appServerState: 'runtime-missing',
|
||||
appServerStatusMessage: 'Codex CLI not found',
|
||||
launchAllowed: false,
|
||||
launchIssueMessage: 'Codex CLI not found',
|
||||
launchReadinessState: 'runtime_missing',
|
||||
},
|
||||
availableBackends: [
|
||||
{
|
||||
id: 'codex-native',
|
||||
label: 'Codex native',
|
||||
description: 'Use the local codex exec JSON seam.',
|
||||
selectable: false,
|
||||
recommended: true,
|
||||
available: false,
|
||||
state: 'runtime-missing',
|
||||
audience: 'general',
|
||||
statusMessage: 'Codex CLI not found',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
initialProviderId: 'codex',
|
||||
codexRuntimeStatus: {
|
||||
installed: false,
|
||||
source: 'missing',
|
||||
state: 'idle',
|
||||
},
|
||||
onInstallCodexRuntime: vi.fn(),
|
||||
onSelectBackend: vi.fn(),
|
||||
onRefreshProvider: vi.fn(() => Promise.resolve(undefined)),
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.textContent).toContain('Install Codex CLI');
|
||||
});
|
||||
|
||||
it('keeps the API key form open and shows an error when delete fails', async () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
import {
|
||||
isCodexProviderRuntimeMissing,
|
||||
shouldOfferCodexRuntimeInstall,
|
||||
} from '@renderer/components/runtime/codexRuntimeInstallAction';
|
||||
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { CliProviderStatus } from '@shared/types';
|
||||
|
||||
function createCodexProvider(overrides?: Partial<CliProviderStatus>): CliProviderStatus {
|
||||
return {
|
||||
providerId: 'codex',
|
||||
displayName: 'Codex',
|
||||
supported: true,
|
||||
authenticated: false,
|
||||
authMethod: null,
|
||||
verificationState: 'error',
|
||||
statusMessage: 'Codex CLI not found',
|
||||
models: [],
|
||||
modelAvailability: [],
|
||||
canLoginFromUi: false,
|
||||
capabilities: {
|
||||
teamLaunch: false,
|
||||
oneShot: false,
|
||||
extensions: createDefaultCliExtensionCapabilities(),
|
||||
},
|
||||
selectedBackendId: 'codex-native',
|
||||
resolvedBackendId: 'codex-native',
|
||||
availableBackends: [
|
||||
{
|
||||
id: 'codex-native',
|
||||
label: 'Codex native',
|
||||
description: 'Use codex exec JSON mode.',
|
||||
selectable: false,
|
||||
recommended: true,
|
||||
available: false,
|
||||
state: 'runtime-missing',
|
||||
audience: 'general',
|
||||
statusMessage: 'Codex CLI not found',
|
||||
},
|
||||
],
|
||||
externalRuntimeDiagnostics: [],
|
||||
backend: null,
|
||||
connection: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('codexRuntimeInstallAction', () => {
|
||||
it('recognizes provider runtime-missing snapshots', () => {
|
||||
expect(isCodexProviderRuntimeMissing(createCodexProvider())).toBe(true);
|
||||
});
|
||||
|
||||
it('does not offer install before installer status is loaded', () => {
|
||||
expect(shouldOfferCodexRuntimeInstall(null)).toBe(false);
|
||||
});
|
||||
|
||||
it('offers install for confirmed missing or failed runtime status only', () => {
|
||||
expect(
|
||||
shouldOfferCodexRuntimeInstall({
|
||||
installed: false,
|
||||
source: 'missing',
|
||||
state: 'idle',
|
||||
})
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldOfferCodexRuntimeInstall({
|
||||
installed: false,
|
||||
source: 'app-managed',
|
||||
state: 'failed',
|
||||
})
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldOfferCodexRuntimeInstall({
|
||||
installed: true,
|
||||
source: 'path',
|
||||
state: 'ready',
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue