diff --git a/src/renderer/utils/teamModelAvailability.ts b/src/renderer/utils/teamModelAvailability.ts index 011c9118..735c4d63 100644 --- a/src/renderer/utils/teamModelAvailability.ts +++ b/src/renderer/utils/teamModelAvailability.ts @@ -306,6 +306,32 @@ function getVisibleRuntimeModels( ); } +function withSupplementalDisabledRuntimeModels( + providerId: SupportedProviderId, + models: readonly string[], + providerStatus?: TeamModelRuntimeProviderStatus | null +): string[] { + if (providerId !== 'codex') { + return [...models]; + } + + const modelSet = new Set(models); + const supplementalDisabledModels = getTeamProviderModelOptions(providerId) + .map((option) => option.value.trim()) + .filter( + (model) => + model.length > 0 && + !modelSet.has(model) && + getTeamModelUiDisabledReason(providerId, model, providerStatus) !== null + ); + + return sortTeamProviderModels( + providerId, + [...models, ...supplementalDisabledModels], + providerStatus + ); +} + function getModelAvailabilityMap( providerStatus?: TeamModelRuntimeProviderStatus | null ): Map { @@ -416,7 +442,11 @@ export function getAvailableTeamProviderModelOptions( return getFallbackTeamProviderModelOptions(providerId, providerStatus); } - const visibleModels = getRuntimeSelectorModels(providerId, providerStatus); + const visibleModels = withSupplementalDisabledRuntimeModels( + providerId, + getRuntimeSelectorModels(providerId, providerStatus), + providerStatus + ); return [ { value: '', label: 'Default', badgeLabel: 'Default' }, ...visibleModels.map((model) => { diff --git a/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts b/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts index d0832abe..3c1e54f1 100644 --- a/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts +++ b/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts @@ -218,7 +218,11 @@ describe('TeamModelSelector disabled Codex models', () => { expect(onValueChange).toHaveBeenCalledWith(''); expect(host.textContent).toContain('5.4'); expect(host.textContent).toContain('5.3 Codex'); - expect(host.textContent).not.toContain('5.2 Codex'); + const disabledCodexButton = Array.from(host.querySelectorAll('button')).find((button) => + button.textContent?.includes('5.2 Codex') + ); + expect(disabledCodexButton).not.toBeNull(); + expect(disabledCodexButton?.getAttribute('aria-disabled')).toBe('true'); await act(async () => { root.unmount(); @@ -703,6 +707,66 @@ describe('TeamModelSelector disabled Codex models', () => { }); }); + it('keeps known disabled Codex tiles visible when the runtime omits them', async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + storeState.cliStatus = { + providers: [ + { + providerId: 'codex', + models: ['gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex', 'gpt-5.2'], + modelVerificationState: 'idle', + modelAvailability: [], + }, + ], + }; + + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + const onValueChange = vi.fn(); + + await act(async () => { + root.render( + React.createElement(TeamModelSelector, { + providerId: 'codex', + onProviderChange: () => undefined, + value: '', + onValueChange, + }) + ); + await Promise.resolve(); + }); + + const disabledButtons = ['5.3 Codex Spark', '5.2 Codex', '5.1 Codex Mini'].map((label) => { + const button = Array.from(host.querySelectorAll('button')).find((candidate) => + candidate.textContent?.includes(label) + ); + expect(button, `${label} should stay visible as a disabled option`).not.toBeNull(); + expect(button?.getAttribute('aria-disabled')).toBe('true'); + expect(button?.textContent).toContain('Disabled'); + expect(button?.getAttribute('title')).toContain('Temporarily disabled for team agents'); + return button; + }); + + const activeButton = Array.from(host.querySelectorAll('button')).find((button) => + button.textContent?.includes('5.2') + ); + expect(activeButton?.textContent).toContain('Recommended'); + expect(activeButton?.getAttribute('aria-disabled')).toBe('false'); + + await act(async () => { + disabledButtons[0]?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await Promise.resolve(); + }); + + expect(onValueChange).not.toHaveBeenCalled(); + + await act(async () => { + root.unmount(); + await Promise.resolve(); + }); + }); + it('keeps 5.1 Codex Max selectable on the native Codex path', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); storeState.cliStatus = {