fix(team): drop unavailable effort selections

This commit is contained in:
777genius 2026-05-22 00:21:17 +03:00
parent a386e30667
commit 3c60ab8653
6 changed files with 143 additions and 37 deletions

View file

@ -74,6 +74,7 @@ import { isGeminiUiFrozen } from '@renderer/utils/geminiUiFreeze';
import { normalizePath } from '@renderer/utils/pathNormalize';
import { resolveUiOwnedProviderBackendId } from '@renderer/utils/providerBackendIdentity';
import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus';
import { getAvailableTeamEffortValue } from '@renderer/utils/teamEffortOptions';
import {
getTeamModelSelectionError,
normalizeExplicitTeamModelForUi,
@ -779,9 +780,27 @@ export const CreateTeamDialog = ({
};
}, []);
const selectedEffortForCurrentSelection = useMemo(
() =>
getAvailableTeamEffortValue({
providerId: selectedProviderId,
model: selectedModel,
limitContext: effectiveAnthropicRuntimeLimitContext,
providerStatus: runtimeProviderStatusById.get(selectedProviderId),
value: selectedEffort,
}),
[
effectiveAnthropicRuntimeLimitContext,
runtimeProviderStatusById,
selectedEffort,
selectedModel,
selectedProviderId,
]
);
const selectedModelChecksByProvider = useMemo(() => {
const modelsByProvider = new Map<TeamProviderId, TeamProvisioningModelCheckRequest[]>();
const leadEffort = (selectedEffort as EffortLevel | '') || undefined;
const leadEffort = (selectedEffortForCurrentSelection as EffortLevel | '') || undefined;
const addModel = (
providerId: TeamProviderId,
model: string | undefined,
@ -850,7 +869,7 @@ export const CreateTeamDialog = ({
effectiveAnthropicRuntimeLimitContext,
effectiveMemberDrafts,
runtimeProviderStatusById,
selectedEffort,
selectedEffortForCurrentSelection,
selectedModel,
selectedProviderId,
]);
@ -1560,13 +1579,13 @@ export const CreateTeamDialog = ({
selectedModel,
limitContext: effectiveAnthropicRuntimeLimitContext,
}),
selectedEffort,
selectedEffort: selectedEffortForCurrentSelection,
selectedFastMode,
providerFastModeDefault: anthropicProviderFastModeDefault,
runtimeCapabilities: runtimeProviderStatusById.get('anthropic')?.runtimeCapabilities,
})
: {
nextEffort: selectedEffort,
nextEffort: selectedEffortForCurrentSelection,
effortResetReason: null,
...reconcileCodexRuntimeSelections({
selection:
@ -1586,7 +1605,11 @@ export const CreateTeamDialog = ({
};
const notices: string[] = [];
if (reconciliation.nextEffort !== selectedEffort) {
if (selectedEffortForCurrentSelection !== selectedEffort) {
setSelectedEffortRaw(selectedEffortForCurrentSelection);
setStoredCreateTeamEffort(selectedEffortForCurrentSelection);
}
if (reconciliation.nextEffort !== selectedEffortForCurrentSelection) {
setSelectedEffortRaw(reconciliation.nextEffort);
setStoredCreateTeamEffort(reconciliation.nextEffort);
if (reconciliation.effortResetReason) {
@ -1608,6 +1631,7 @@ export const CreateTeamDialog = ({
effectiveAnthropicRuntimeLimitContext,
runtimeProviderStatusById,
selectedEffort,
selectedEffortForCurrentSelection,
selectedFastMode,
selectedModel,
selectedProviderId,
@ -1634,7 +1658,7 @@ export const CreateTeamDialog = ({
providerId: selectedProviderId,
providerBackendId: selectedProviderBackendId ?? undefined,
model: effectiveModel,
effort: (selectedEffort as EffortLevel) || undefined,
effort: (selectedEffortForCurrentSelection as EffortLevel) || undefined,
fastMode:
selectedProviderId === 'anthropic' || selectedProviderId === 'codex'
? selectedFastMode
@ -1655,7 +1679,7 @@ export const CreateTeamDialog = ({
selectedProviderId,
selectedProviderBackendId,
effectiveModel,
selectedEffort,
selectedEffortForCurrentSelection,
selectedFastMode,
effectiveAnthropicRuntimeLimitContext,
skipPermissions,
@ -1768,8 +1792,8 @@ export const CreateTeamDialog = ({
if (effectiveModel) args.push('--model', effectiveModel);
const effectiveEffort =
selectedProviderId === 'anthropic'
? selectedEffort || anthropicRuntimeSelection?.defaultEffort || ''
: selectedEffort;
? selectedEffortForCurrentSelection || anthropicRuntimeSelection?.defaultEffort || ''
: selectedEffortForCurrentSelection;
if (effectiveEffort) args.push('--effort', effectiveEffort);
if (selectedProviderId === 'anthropic') {
const fastSettings = anthropicFastModeResolution?.resolvedFastMode
@ -1785,7 +1809,7 @@ export const CreateTeamDialog = ({
anthropicRuntimeSelection?.defaultEffort,
codexFastModeResolution?.resolvedFastMode,
effectiveModel,
selectedEffort,
selectedEffortForCurrentSelection,
selectedProviderId,
skipPermissions,
]);
@ -2172,7 +2196,7 @@ export const CreateTeamDialog = ({
defaultProviderId={selectedProviderId}
inheritedProviderId={selectedProviderId}
inheritedModel={selectedModel}
inheritedEffort={(selectedEffort as EffortLevel) || undefined}
inheritedEffort={(selectedEffortForCurrentSelection as EffortLevel) || undefined}
inheritModelSettingsByDefault
lockProviderModel={syncModelsWithLead}
forceInheritedModelSettings={syncModelsWithLead}
@ -2180,7 +2204,7 @@ export const CreateTeamDialog = ({
hideMembersContent={soloTeam}
providerId={selectedProviderId}
model={selectedModel}
effort={(selectedEffort as EffortLevel) || undefined}
effort={(selectedEffortForCurrentSelection as EffortLevel) || undefined}
limitContext={effectiveAnthropicRuntimeLimitContext}
leadProviderNoticeById={teammateRuntimeProviderNoticeById}
onProviderChange={setSelectedProviderId}

View file

@ -3,7 +3,10 @@ import React, { useEffect, useMemo } from 'react';
import { Label } from '@renderer/components/ui/label';
import { useEffectiveCliProviderStatus } from '@renderer/hooks/useEffectiveCliProviderStatus';
import { cn } from '@renderer/lib/utils';
import { getTeamEffortSelectorPresentation } from '@renderer/utils/teamEffortOptions';
import {
getAvailableTeamEffortValue,
getTeamEffortSelectorPresentation,
} from '@renderer/utils/teamEffortOptions';
import { Brain } from 'lucide-react';
import type { TeamProviderId } from '@shared/types';
@ -33,6 +36,13 @@ export const EffortLevelSelector: React.FC<EffortLevelSelectorProps> = ({
providerStatus,
});
const effortOptions = presentation.options;
const displayValue = getAvailableTeamEffortValue({
providerId,
model,
limitContext,
providerStatus,
value,
});
const validValues = useMemo(
() => new Set(effortOptions.map((option) => option.value)),
[effortOptions]
@ -59,13 +69,13 @@ export const EffortLevelSelector: React.FC<EffortLevelSelectorProps> = ({
<button
key={opt.value || '__default__'}
type="button"
id={opt.value === value ? id : undefined}
id={opt.value === displayValue ? id : undefined}
disabled={presentation.disabled}
className={cn(
'rounded-[3px] px-3 py-1 text-xs font-medium transition-colors',
presentation.disabled
? 'cursor-not-allowed text-[var(--color-text-muted)] opacity-70'
: value === opt.value
: displayValue === opt.value
? 'bg-[var(--color-surface-raised)] text-[var(--color-text)] shadow-sm'
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]'
)}

View file

@ -64,6 +64,7 @@ import { normalizePath } from '@renderer/utils/pathNormalize';
import { nameColorSet } from '@renderer/utils/projectColor';
import { resolveUiOwnedProviderBackendId } from '@renderer/utils/providerBackendIdentity';
import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus';
import { getAvailableTeamEffortValue } from '@renderer/utils/teamEffortOptions';
import {
getTeamModelSelectionError,
normalizeExplicitTeamModelForUi,
@ -1081,6 +1082,24 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
[codexRuntimeSelection, selectedFastMode, selectedProviderId]
);
const selectedEffortForCurrentSelection = useMemo(
() =>
getAvailableTeamEffortValue({
providerId: selectedProviderId,
model: selectedModel,
limitContext: effectiveAnthropicRuntimeLimitContext,
providerStatus: runtimeProviderStatusById.get(selectedProviderId),
value: selectedEffort,
}),
[
effectiveAnthropicRuntimeLimitContext,
runtimeProviderStatusById,
selectedEffort,
selectedModel,
selectedProviderId,
]
);
useEffect(() => {
if (isSchedule && schedule) {
const nextHydrationKey = `${schedule.id}:${schedule.updatedAt ?? ''}`;
@ -1107,13 +1126,13 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
selectedModel,
limitContext: effectiveAnthropicRuntimeLimitContext,
}),
selectedEffort,
selectedEffort: selectedEffortForCurrentSelection,
selectedFastMode,
providerFastModeDefault: anthropicProviderFastModeDefault,
runtimeCapabilities: runtimeProviderStatusById.get('anthropic')?.runtimeCapabilities,
})
: {
nextEffort: selectedEffort,
nextEffort: selectedEffortForCurrentSelection,
effortResetReason: null,
...reconcileCodexRuntimeSelections({
selection:
@ -1139,7 +1158,11 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
};
const notices: string[] = [];
if (reconciliation.nextEffort !== selectedEffort) {
if (selectedEffortForCurrentSelection !== selectedEffort) {
setSelectedEffortRaw(selectedEffortForCurrentSelection);
localStorage.setItem('team:lastSelectedEffort', selectedEffortForCurrentSelection);
}
if (reconciliation.nextEffort !== selectedEffortForCurrentSelection) {
setSelectedEffortRaw(reconciliation.nextEffort);
localStorage.setItem('team:lastSelectedEffort', reconciliation.nextEffort);
if (reconciliation.effortResetReason) {
@ -1163,6 +1186,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
runtimeProviderStatusById,
savedLaunchProviderBackendId,
selectedEffort,
selectedEffortForCurrentSelection,
selectedFastMode,
selectedModel,
selectedProviderId,
@ -1173,7 +1197,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
const selectedModelChecksByProvider = useMemo(() => {
const modelsByProvider = new Map<TeamProviderId, TeamProvisioningModelCheckRequest[]>();
const leadEffort = (selectedEffort as EffortLevel | '') || undefined;
const leadEffort = (selectedEffortForCurrentSelection as EffortLevel | '') || undefined;
const addModel = (
providerId: TeamProviderId,
model: string | undefined,
@ -1237,7 +1261,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
effectiveLeadRuntimeModel,
effectiveMemberDrafts,
runtimeProviderStatusById,
selectedEffort,
selectedEffortForCurrentSelection,
selectedModel,
selectedProviderId,
]);
@ -1256,7 +1280,8 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
previousProviderId &&
(previousProviderId !== selectedProviderId ||
previousLeadModel !== currentLeadDisplayModel ||
(previousLeadEffort ?? '') !== ((selectedEffort as EffortLevel | '') || ''))
(previousLeadEffort ?? '') !==
((selectedEffortForCurrentSelection as EffortLevel | '') || ''))
) {
notes.push({
key: 'lead',
@ -1264,7 +1289,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
message: `${formatTeamModelSummary(
selectedProviderId,
currentLeadDisplayModel,
(selectedEffort as EffortLevel) || undefined
(selectedEffortForCurrentSelection as EffortLevel) || undefined
)} instead of ${formatTeamModelSummary(
previousProviderId,
previousLeadModel,
@ -1300,7 +1325,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
member,
selectedProviderId,
currentLeadDisplayModel,
(selectedEffort as EffortLevel) || undefined
(selectedEffortForCurrentSelection as EffortLevel) || undefined
);
const {
@ -1358,7 +1383,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
selectedProviderId,
selectedModel,
effectiveLeadRuntimeModel,
selectedEffort,
selectedEffortForCurrentSelection,
members,
effectiveMemberDrafts,
]);
@ -1907,8 +1932,8 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
if (model) args.push('--model', model);
const effectiveEffort =
selectedProviderId === 'anthropic'
? selectedEffort || anthropicRuntimeSelection?.defaultEffort || ''
: selectedEffort;
? selectedEffortForCurrentSelection || anthropicRuntimeSelection?.defaultEffort || ''
: selectedEffortForCurrentSelection;
if (effectiveEffort) args.push('--effort', effectiveEffort);
if (selectedProviderId === 'anthropic') {
const fastSettings = anthropicFastModeResolution?.resolvedFastMode
@ -1927,7 +1952,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
skipPermissions,
selectedModel,
effectiveAnthropicRuntimeLimitContext,
selectedEffort,
selectedEffortForCurrentSelection,
selectedProviderId,
runtimeProviderStatusById,
]);
@ -1947,7 +1972,9 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
}
summary.push(`Provider: ${getProviderLabel(selectedProviderId)}`);
if (selectedModel) summary.push(`Model: ${selectedModel}`);
if (selectedEffort) summary.push(`Effort: ${selectedEffort}`);
if (selectedEffortForCurrentSelection) {
summary.push(`Effort: ${selectedEffortForCurrentSelection}`);
}
if (selectedProviderId === 'anthropic' || selectedProviderId === 'codex') {
if (selectedFastMode === 'on') summary.push('Fast mode');
else if (selectedFastMode === 'off') summary.push('Fast disabled');
@ -1969,7 +1996,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
promptDraft.value,
selectedModel,
selectedProviderId,
selectedEffort,
selectedEffortForCurrentSelection,
selectedFastMode,
anthropicProviderFastModeDefault,
effectiveAnthropicRuntimeLimitContext,
@ -2222,7 +2249,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
selectedProviderId,
runtimeProviderStatusById.get(selectedProviderId)
),
effort: (selectedEffort as EffortLevel) || undefined,
effort: (selectedEffortForCurrentSelection as EffortLevel) || undefined,
fastMode:
selectedProviderId === 'anthropic' || selectedProviderId === 'codex'
? selectedFastMode
@ -2258,8 +2285,8 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
selectedProviderId,
runtimeProviderStatusById.get(selectedProviderId)
);
const explicitScheduleEffort = selectedEffort
? (selectedEffort as EffortLevel)
const explicitScheduleEffort = selectedEffortForCurrentSelection
? (selectedEffortForCurrentSelection as EffortLevel)
: undefined;
const scheduleEffort =
selectedProviderId === 'anthropic'
@ -2687,14 +2714,14 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
defaultProviderId={selectedProviderId}
inheritedProviderId={selectedProviderId}
inheritedModel={selectedModel}
inheritedEffort={(selectedEffort as EffortLevel) || undefined}
inheritedEffort={(selectedEffortForCurrentSelection as EffortLevel) || undefined}
inheritModelSettingsByDefault
lockProviderModel={syncModelsWithLead}
forceInheritedModelSettings={syncModelsWithLead}
modelLockReason="This teammate is synced with the lead model. Turn off sync to set a custom provider, model, or effort."
providerId={selectedProviderId}
model={selectedModel}
effort={(selectedEffort as EffortLevel) || undefined}
effort={(selectedEffortForCurrentSelection as EffortLevel) || undefined}
limitContext={effectiveAnthropicRuntimeLimitContext}
leadProviderNoticeById={teammateRuntimeProviderNoticeById}
onProviderChange={setSelectedProviderId}
@ -2893,7 +2920,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
}}
/>
<EffortLevelSelector
value={selectedEffort}
value={selectedEffortForCurrentSelection}
onValueChange={setSelectedEffort}
id="dialog-effort"
providerId={selectedProviderId}

View file

@ -1,6 +1,10 @@
import { describe, expect, it } from 'vitest';
import { getTeamEffortOptions, getTeamEffortSelectorPresentation } from '../teamEffortOptions';
import {
getAvailableTeamEffortValue,
getTeamEffortOptions,
getTeamEffortSelectorPresentation,
} from '../teamEffortOptions';
import type { CliProviderStatus } from '@shared/types';
@ -326,4 +330,29 @@ describe('team effort options', () => {
unavailableText: 'Effort is unavailable for this model.',
});
});
it('omits stale Anthropic effort when the selected model has no effort support', () => {
const providerStatus = createProviderStatus('anthropic', {
id: 'haiku',
launchModel: 'claude-haiku-4-5-20251001',
displayName: 'Haiku 4.5',
hidden: false,
supportedReasoningEfforts: [],
defaultReasoningEffort: null,
inputModalities: ['text', 'image'],
supportsPersonality: false,
isDefault: false,
upgrade: false,
source: 'anthropic-models-api',
});
expect(
getAvailableTeamEffortValue({
providerId: 'anthropic',
model: 'claude-haiku-4-5-20251001',
providerStatus,
value: 'medium',
})
).toBe('');
});
});

View file

@ -213,3 +213,19 @@ export function getTeamEffortSelectorPresentation(params: {
canValidateValue: true,
};
}
export function getAvailableTeamEffortValue(params: {
providerId?: TeamProviderId;
model?: string;
limitContext?: boolean;
providerStatus?: CliProviderStatus | null;
value?: string | null;
}): string {
const value = params.value?.trim() ?? '';
if (!value) {
return '';
}
const presentation = getTeamEffortSelectorPresentation(params);
return presentation.disabled && presentation.canValidateValue ? '' : value;
}

View file

@ -1,8 +1,8 @@
import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { EffortLevelSelector } from '@renderer/components/team/dialogs/EffortLevelSelector';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { CliProviderStatus } from '@shared/types';