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,
|
CodexLoginLinkCopyButton,
|
||||||
CodexLoginUserCodeBadge,
|
CodexLoginUserCodeBadge,
|
||||||
} from '@renderer/components/runtime/CodexLoginLinkCopyButton';
|
} from '@renderer/components/runtime/CodexLoginLinkCopyButton';
|
||||||
|
import {
|
||||||
|
isCodexProviderRuntimeMissing,
|
||||||
|
shouldOfferCodexRuntimeInstall,
|
||||||
|
} from '@renderer/components/runtime/codexRuntimeInstallAction';
|
||||||
import {
|
import {
|
||||||
formatProviderStatusText,
|
formatProviderStatusText,
|
||||||
getProviderConnectionModeSummary,
|
getProviderConnectionModeSummary,
|
||||||
|
|
@ -606,30 +610,12 @@ function shouldShowCodexInstallAction(
|
||||||
showSkeleton: boolean,
|
showSkeleton: boolean,
|
||||||
codexRuntimeStatus: CodexRuntimeStatus | null
|
codexRuntimeStatus: CodexRuntimeStatus | null
|
||||||
): boolean {
|
): 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 (
|
return (
|
||||||
provider.providerId === 'codex' &&
|
provider.providerId === 'codex' &&
|
||||||
!showSkeleton &&
|
!showSkeleton &&
|
||||||
!provider.authenticated &&
|
!provider.authenticated &&
|
||||||
runtimeMissing &&
|
isCodexProviderRuntimeMissing(provider) &&
|
||||||
!(codexRuntimeStatus?.source === 'app-managed' && codexRuntimeStatus.state !== 'failed')
|
shouldOfferCodexRuntimeInstall(codexRuntimeStatus)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1368,6 +1354,7 @@ export const CliStatusBanner = (): React.JSX.Element | null => {
|
||||||
bootstrapCliStatus,
|
bootstrapCliStatus,
|
||||||
fetchCliStatus,
|
fetchCliStatus,
|
||||||
fetchCliProviderStatus,
|
fetchCliProviderStatus,
|
||||||
|
fetchCodexRuntimeStatus,
|
||||||
invalidateCliStatus,
|
invalidateCliStatus,
|
||||||
installCli,
|
installCli,
|
||||||
installOpenCodeRuntime,
|
installOpenCodeRuntime,
|
||||||
|
|
@ -1455,6 +1442,23 @@ export const CliStatusBanner = (): React.JSX.Element | null => {
|
||||||
[loadingCliStatus, visibleCliProviders]
|
[loadingCliStatus, visibleCliProviders]
|
||||||
);
|
);
|
||||||
const renderCliStatus = effectiveCliStatus;
|
const renderCliStatus = effectiveCliStatus;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isElectron || codexRuntimeStatus || codexRuntimeStatusLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visibleCliProviders.some(isCodexProviderRuntimeMissing)) {
|
||||||
|
void fetchCodexRuntimeStatus();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
codexRuntimeStatus,
|
||||||
|
codexRuntimeStatusLoading,
|
||||||
|
fetchCodexRuntimeStatus,
|
||||||
|
isElectron,
|
||||||
|
visibleCliProviders,
|
||||||
|
]);
|
||||||
|
|
||||||
const shouldPollAnthropicSubscriptionLimits = useMemo(() => {
|
const shouldPollAnthropicSubscriptionLimits = useMemo(() => {
|
||||||
if (
|
if (
|
||||||
!renderCliStatus?.installed ||
|
!renderCliStatus?.installed ||
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,10 @@ import { Tabs, TabsList, TabsTrigger } from '@renderer/components/ui/tabs';
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
import { AlertTriangle, Download, Key, Link2, Loader2, Save, Trash2 } from 'lucide-react';
|
import { AlertTriangle, Download, Key, Link2, Loader2, Save, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
isCodexProviderRuntimeMissing,
|
||||||
|
shouldOfferCodexRuntimeInstall,
|
||||||
|
} from './codexRuntimeInstallAction';
|
||||||
import {
|
import {
|
||||||
formatProviderAuthMethodLabelForProvider,
|
formatProviderAuthMethodLabelForProvider,
|
||||||
formatProviderAuthModeLabelForProvider,
|
formatProviderAuthModeLabelForProvider,
|
||||||
|
|
@ -1056,8 +1060,8 @@ export const ProviderRuntimeSettingsDialog = ({
|
||||||
const showCodexRuntimeInstallAction =
|
const showCodexRuntimeInstallAction =
|
||||||
selectedProvider?.providerId === 'codex' &&
|
selectedProvider?.providerId === 'codex' &&
|
||||||
typeof onInstallCodexRuntime === 'function' &&
|
typeof onInstallCodexRuntime === 'function' &&
|
||||||
codexConnection?.appServerState === 'runtime-missing' &&
|
isCodexProviderRuntimeMissing(selectedProvider) &&
|
||||||
!(codexRuntimeStatus?.source === 'app-managed' && codexRuntimeStatus.state !== 'failed');
|
shouldOfferCodexRuntimeInstall(codexRuntimeStatus);
|
||||||
const runtimeBusy = disabled || selectedProviderLoading || runtimeSaving;
|
const runtimeBusy = disabled || selectedProviderLoading || runtimeSaving;
|
||||||
const anthropicFastModeCapability =
|
const anthropicFastModeCapability =
|
||||||
selectedProvider?.providerId === 'anthropic'
|
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 { isElectronMode } from '@renderer/api';
|
||||||
import { confirm } from '@renderer/components/common/ConfirmDialog';
|
import { confirm } from '@renderer/components/common/ConfirmDialog';
|
||||||
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
||||||
|
import { isCodexProviderRuntimeMissing } from '@renderer/components/runtime/codexRuntimeInstallAction';
|
||||||
import {
|
import {
|
||||||
formatProviderStatusText,
|
formatProviderStatusText,
|
||||||
getProviderConnectionModeSummary,
|
getProviderConnectionModeSummary,
|
||||||
|
|
@ -148,6 +149,7 @@ export const CliStatusSection = (): React.JSX.Element | null => {
|
||||||
bootstrapCliStatus,
|
bootstrapCliStatus,
|
||||||
fetchCliStatus,
|
fetchCliStatus,
|
||||||
fetchCliProviderStatus,
|
fetchCliProviderStatus,
|
||||||
|
fetchCodexRuntimeStatus,
|
||||||
installCodexRuntime,
|
installCodexRuntime,
|
||||||
installCli,
|
installCli,
|
||||||
isBusy,
|
isBusy,
|
||||||
|
|
@ -226,6 +228,22 @@ export const CliStatusSection = (): React.JSX.Element | null => {
|
||||||
}
|
}
|
||||||
}, [bootstrapCliStatus, cliStatus, fetchCliStatus, isElectron, multimodelEnabled]);
|
}, [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(() => {
|
const handleInstall = useCallback(() => {
|
||||||
installCli();
|
installCli();
|
||||||
}, [installCli]);
|
}, [installCli]);
|
||||||
|
|
|
||||||
|
|
@ -1755,6 +1755,110 @@ describe('ProviderRuntimeSettingsDialog', () => {
|
||||||
expect(icon?.className).toContain('shrink-0');
|
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 () => {
|
it('keeps the API key form open and shows an error when delete fails', async () => {
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
document.body.appendChild(host);
|
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