feat(team-ui): freeze unstable provider and model options
This commit is contained in:
parent
433bdf8bbc
commit
a03c22aace
14 changed files with 455 additions and 88 deletions
|
|
@ -20,6 +20,7 @@ import { TerminalModal } from '@renderer/components/terminal/TerminalModal';
|
||||||
import { useCliInstaller } from '@renderer/hooks/useCliInstaller';
|
import { useCliInstaller } from '@renderer/hooks/useCliInstaller';
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
import { formatBytes } from '@renderer/utils/formatters';
|
import { formatBytes } from '@renderer/utils/formatters';
|
||||||
|
import { filterMainScreenCliProviders } from '@renderer/utils/geminiUiFreeze';
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
|
|
@ -338,18 +339,19 @@ function formatRuntimeLabel(
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatRuntimeAuthSummary(
|
function formatRuntimeAuthSummary(
|
||||||
cliStatus: NonNullable<ReturnType<typeof useCliInstaller>['cliStatus']>
|
cliStatus: NonNullable<ReturnType<typeof useCliInstaller>['cliStatus']>,
|
||||||
|
visibleProviders: readonly CliProviderStatus[]
|
||||||
): string | null {
|
): string | null {
|
||||||
if (cliStatus.flavor === 'free-code' && cliStatus.providers.length > 0) {
|
if (cliStatus.flavor === 'free-code' && visibleProviders.length > 0) {
|
||||||
if (
|
if (
|
||||||
cliStatus.providers.every(
|
visibleProviders.every(
|
||||||
(provider) => provider.statusMessage === 'Checking...' && !provider.authenticated
|
(provider) => provider.statusMessage === 'Checking...' && !provider.authenticated
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return 'Checking providers...';
|
return 'Checking providers...';
|
||||||
}
|
}
|
||||||
const denominator = cliStatus.providers.length;
|
const denominator = visibleProviders.length;
|
||||||
const connected = cliStatus.providers.filter((provider) => provider.authenticated).length;
|
const connected = visibleProviders.filter((provider) => provider.authenticated).length;
|
||||||
|
|
||||||
return `Providers: ${connected}/${denominator} connected`;
|
return `Providers: ${connected}/${denominator} connected`;
|
||||||
}
|
}
|
||||||
|
|
@ -366,12 +368,13 @@ function formatRuntimeAuthSummary(
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCheckingMultimodelStatus(
|
function isCheckingMultimodelStatus(
|
||||||
cliStatus: NonNullable<ReturnType<typeof useCliInstaller>['cliStatus']>
|
cliStatus: NonNullable<ReturnType<typeof useCliInstaller>['cliStatus']>,
|
||||||
|
visibleProviders: readonly CliProviderStatus[]
|
||||||
): boolean {
|
): boolean {
|
||||||
return (
|
return (
|
||||||
cliStatus.flavor === 'free-code' &&
|
cliStatus.flavor === 'free-code' &&
|
||||||
cliStatus.providers.length > 0 &&
|
visibleProviders.length > 0 &&
|
||||||
cliStatus.providers.every(
|
visibleProviders.every(
|
||||||
(provider) => provider.statusMessage === 'Checking...' && !provider.authenticated
|
(provider) => provider.statusMessage === 'Checking...' && !provider.authenticated
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
@ -396,8 +399,12 @@ const InstalledBanner = ({
|
||||||
}: InstalledBannerProps): React.JSX.Element => {
|
}: InstalledBannerProps): React.JSX.Element => {
|
||||||
const openExtensionsTab = useStore((s) => s.openExtensionsTab);
|
const openExtensionsTab = useStore((s) => s.openExtensionsTab);
|
||||||
const styles = VARIANT_STYLES[variant];
|
const styles = VARIANT_STYLES[variant];
|
||||||
|
const visibleProviders = useMemo(
|
||||||
|
() => filterMainScreenCliProviders(cliStatus.providers),
|
||||||
|
[cliStatus.providers]
|
||||||
|
);
|
||||||
const runtimeLabel = formatRuntimeLabel(cliStatus);
|
const runtimeLabel = formatRuntimeLabel(cliStatus);
|
||||||
const runtimeAuthSummary = formatRuntimeAuthSummary(cliStatus);
|
const runtimeAuthSummary = formatRuntimeAuthSummary(cliStatus, visibleProviders);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -498,12 +505,12 @@ const InstalledBanner = ({
|
||||||
Failed to check for updates. Check your network connection and try again.
|
Failed to check for updates. Check your network connection and try again.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{cliStatus.providers.length > 0 && (
|
{visibleProviders.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className="mt-3 space-y-2 border-t pt-3"
|
className="mt-3 space-y-2 border-t pt-3"
|
||||||
style={{ borderColor: 'var(--color-border-subtle)' }}
|
style={{ borderColor: 'var(--color-border-subtle)' }}
|
||||||
>
|
>
|
||||||
{cliStatus.providers.map((provider) => {
|
{visibleProviders.map((provider) => {
|
||||||
const statusText = formatProviderStatus(provider);
|
const statusText = formatProviderStatus(provider);
|
||||||
const actionDisabled = isBusy || !cliStatus.binaryPath;
|
const actionDisabled = isBusy || !cliStatus.binaryPath;
|
||||||
const runtimeSummary = getProviderRuntimeBackendSummary(provider);
|
const runtimeSummary = getProviderRuntimeBackendSummary(provider);
|
||||||
|
|
@ -661,12 +668,16 @@ export const CliStatusBanner = (): React.JSX.Element | null => {
|
||||||
providerId: CliProviderId;
|
providerId: CliProviderId;
|
||||||
action: 'login' | 'logout';
|
action: 'login' | 'logout';
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [manageProviderId, setManageProviderId] = useState<CliProviderId>('gemini');
|
const [manageProviderId, setManageProviderId] = useState<CliProviderId>('anthropic');
|
||||||
const [manageDialogOpen, setManageDialogOpen] = useState(false);
|
const [manageDialogOpen, setManageDialogOpen] = useState(false);
|
||||||
const [isVerifyingAuth, setIsVerifyingAuth] = useState(false);
|
const [isVerifyingAuth, setIsVerifyingAuth] = useState(false);
|
||||||
const [isSwitchingFlavor, setIsSwitchingFlavor] = useState(false);
|
const [isSwitchingFlavor, setIsSwitchingFlavor] = useState(false);
|
||||||
const [showTroubleshoot, setShowTroubleshoot] = useState(false);
|
const [showTroubleshoot, setShowTroubleshoot] = useState(false);
|
||||||
const multimodelEnabled = appConfig?.general?.multimodelEnabled ?? true;
|
const multimodelEnabled = appConfig?.general?.multimodelEnabled ?? true;
|
||||||
|
const visibleCliProviders = useMemo(
|
||||||
|
() => filterMainScreenCliProviders(cliStatus?.providers ?? []),
|
||||||
|
[cliStatus?.providers]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isElectron) return;
|
if (!isElectron) return;
|
||||||
|
|
@ -799,7 +810,7 @@ export const CliStatusBanner = (): React.JSX.Element | null => {
|
||||||
if (installerState === 'completed') return 'success';
|
if (installerState === 'completed') return 'success';
|
||||||
if (installerState !== 'idle') return 'info';
|
if (installerState !== 'idle') return 'info';
|
||||||
if (!cliStatus) return 'loading';
|
if (!cliStatus) return 'loading';
|
||||||
if (isCheckingMultimodelStatus(cliStatus)) return 'info';
|
if (isCheckingMultimodelStatus(cliStatus, visibleCliProviders)) return 'info';
|
||||||
if (cliStatus.authStatusChecking) return 'info';
|
if (cliStatus.authStatusChecking) return 'info';
|
||||||
if (!cliStatus.installed) return 'error';
|
if (!cliStatus.installed) return 'error';
|
||||||
if (cliStatus.installed && !cliStatus.authLoggedIn) return 'warning';
|
if (cliStatus.installed && !cliStatus.authLoggedIn) return 'warning';
|
||||||
|
|
@ -1293,8 +1304,12 @@ export const CliStatusBanner = (): React.JSX.Element | null => {
|
||||||
<ProviderRuntimeSettingsDialog
|
<ProviderRuntimeSettingsDialog
|
||||||
open={manageDialogOpen}
|
open={manageDialogOpen}
|
||||||
onOpenChange={setManageDialogOpen}
|
onOpenChange={setManageDialogOpen}
|
||||||
providers={cliStatus.providers}
|
providers={visibleCliProviders}
|
||||||
initialProviderId={manageProviderId}
|
initialProviderId={
|
||||||
|
visibleCliProviders.some((provider) => provider.providerId === manageProviderId)
|
||||||
|
? manageProviderId
|
||||||
|
: (visibleCliProviders[0]?.providerId ?? 'anthropic')
|
||||||
|
}
|
||||||
providerStatusLoading={cliProviderStatusLoading}
|
providerStatusLoading={cliProviderStatusLoading}
|
||||||
disabled={isBusy || cliStatusLoading || !cliStatus.binaryPath}
|
disabled={isBusy || cliStatusLoading || !cliStatus.binaryPath}
|
||||||
onSelectBackend={(providerId, backendId) => {
|
onSelectBackend={(providerId, backendId) => {
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,12 @@ import { useTeamSuggestions } from '@renderer/hooks/useTeamSuggestions';
|
||||||
import { useTheme } from '@renderer/hooks/useTheme';
|
import { useTheme } from '@renderer/hooks/useTheme';
|
||||||
import { cn } from '@renderer/lib/utils';
|
import { cn } from '@renderer/lib/utils';
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
|
import {
|
||||||
|
isGeminiUiFrozen,
|
||||||
|
normalizeCreateLaunchProviderForUi,
|
||||||
|
} from '@renderer/utils/geminiUiFreeze';
|
||||||
import { normalizePath } from '@renderer/utils/pathNormalize';
|
import { normalizePath } from '@renderer/utils/pathNormalize';
|
||||||
|
import { normalizeTeamModelForUi } from '@renderer/utils/teamModelAvailability';
|
||||||
import { isTeamProviderId, normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
import { isTeamProviderId, normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
||||||
import { AlertTriangle, CheckCircle2, Info, Loader2, X } from 'lucide-react';
|
import { AlertTriangle, CheckCircle2, Info, Loader2, X } from 'lucide-react';
|
||||||
|
|
||||||
|
|
@ -81,7 +86,11 @@ import type {
|
||||||
|
|
||||||
function getStoredTeamProvider(): TeamProviderId {
|
function getStoredTeamProvider(): TeamProviderId {
|
||||||
const stored = localStorage.getItem('team:lastSelectedProvider');
|
const stored = localStorage.getItem('team:lastSelectedProvider');
|
||||||
return stored === 'codex' || stored === 'gemini' ? stored : 'anthropic';
|
// return stored === 'codex' || stored === 'gemini' ? stored : 'anthropic';
|
||||||
|
return normalizeCreateLaunchProviderForUi(
|
||||||
|
stored === 'codex' || stored === 'gemini' ? stored : 'anthropic',
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStoredTeamModel(providerId: TeamProviderId): string {
|
function getStoredTeamModel(providerId: TeamProviderId): string {
|
||||||
|
|
@ -89,7 +98,7 @@ function getStoredTeamModel(providerId: TeamProviderId): string {
|
||||||
if (stored === null) {
|
if (stored === null) {
|
||||||
return providerId === 'anthropic' ? 'opus' : '';
|
return providerId === 'anthropic' ? 'opus' : '';
|
||||||
}
|
}
|
||||||
return stored === '__default__' ? '' : stored;
|
return normalizeTeamModelForUi(providerId, stored === '__default__' ? '' : stored);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEphemeralRenderedProjectPath(projectPath: string | null | undefined): boolean {
|
function isEphemeralRenderedProjectPath(projectPath: string | null | undefined): boolean {
|
||||||
|
|
@ -368,8 +377,9 @@ export const CreateTeamDialog = ({
|
||||||
}, [advancedKey]);
|
}, [advancedKey]);
|
||||||
|
|
||||||
const setSelectedModel = (value: string): void => {
|
const setSelectedModel = (value: string): void => {
|
||||||
setSelectedModelRaw(value);
|
const normalizedValue = normalizeTeamModelForUi(selectedProviderId, value);
|
||||||
localStorage.setItem(`team:lastSelectedModel:${selectedProviderId}`, value);
|
setSelectedModelRaw(normalizedValue);
|
||||||
|
localStorage.setItem(`team:lastSelectedModel:${selectedProviderId}`, normalizedValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setSelectedProviderId = (value: TeamProviderId): void => {
|
const setSelectedProviderId = (value: TeamProviderId): void => {
|
||||||
|
|
@ -1160,6 +1170,7 @@ export const CreateTeamDialog = ({
|
||||||
onLimitContextChange={setLimitContext}
|
onLimitContextChange={setLimitContext}
|
||||||
syncModelsWithTeammates={syncModelsWithLead}
|
syncModelsWithTeammates={syncModelsWithLead}
|
||||||
onSyncModelsWithTeammatesChange={handleSyncModelsWithLeadChange}
|
onSyncModelsWithTeammatesChange={handleSyncModelsWithLeadChange}
|
||||||
|
disableGeminiOption={isGeminiUiFrozen()}
|
||||||
headerTop={
|
headerTop={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,11 @@ import { useTaskSuggestions } from '@renderer/hooks/useTaskSuggestions';
|
||||||
import { useTeamSuggestions } from '@renderer/hooks/useTeamSuggestions';
|
import { useTeamSuggestions } from '@renderer/hooks/useTeamSuggestions';
|
||||||
import { useTheme } from '@renderer/hooks/useTheme';
|
import { useTheme } from '@renderer/hooks/useTheme';
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
|
import {
|
||||||
|
isGeminiUiFrozen,
|
||||||
|
normalizeCreateLaunchProviderForUi,
|
||||||
|
} from '@renderer/utils/geminiUiFreeze';
|
||||||
|
import { normalizeTeamModelForUi } from '@renderer/utils/teamModelAvailability';
|
||||||
import { isTeamProviderId, normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
import { isTeamProviderId, normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
||||||
import {
|
import {
|
||||||
getCurrentProvisioningProgressForTeam,
|
getCurrentProvisioningProgressForTeam,
|
||||||
|
|
@ -140,7 +145,11 @@ function getLocalTimezone(): string {
|
||||||
|
|
||||||
function getStoredTeamProvider(): TeamProviderId {
|
function getStoredTeamProvider(): TeamProviderId {
|
||||||
const stored = localStorage.getItem('team:lastSelectedProvider');
|
const stored = localStorage.getItem('team:lastSelectedProvider');
|
||||||
return stored === 'codex' || stored === 'gemini' ? stored : 'anthropic';
|
// return stored === 'codex' || stored === 'gemini' ? stored : 'anthropic';
|
||||||
|
return normalizeCreateLaunchProviderForUi(
|
||||||
|
stored === 'codex' || stored === 'gemini' ? stored : 'anthropic',
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStoredTeamModel(providerId: TeamProviderId): string {
|
function getStoredTeamModel(providerId: TeamProviderId): string {
|
||||||
|
|
@ -148,7 +157,7 @@ function getStoredTeamModel(providerId: TeamProviderId): string {
|
||||||
if (stored === null) {
|
if (stored === null) {
|
||||||
return providerId === 'anthropic' ? 'opus' : '';
|
return providerId === 'anthropic' ? 'opus' : '';
|
||||||
}
|
}
|
||||||
return stored === '__default__' ? '' : stored;
|
return normalizeTeamModelForUi(providerId, stored === '__default__' ? '' : stored);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProviderLabel(providerId: TeamProviderId): string {
|
function getProviderLabel(providerId: TeamProviderId): string {
|
||||||
|
|
@ -401,8 +410,9 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
};
|
};
|
||||||
|
|
||||||
const setSelectedModel = (value: string): void => {
|
const setSelectedModel = (value: string): void => {
|
||||||
setSelectedModelRaw(value);
|
const normalizedValue = normalizeTeamModelForUi(selectedProviderId, value);
|
||||||
localStorage.setItem(`team:lastSelectedModel:${selectedProviderId}`, value);
|
setSelectedModelRaw(normalizedValue);
|
||||||
|
localStorage.setItem(`team:lastSelectedModel:${selectedProviderId}`, normalizedValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setLimitContext = (value: boolean): void => {
|
const setLimitContext = (value: boolean): void => {
|
||||||
|
|
@ -496,7 +506,8 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
);
|
);
|
||||||
setSelectedProviderIdRaw(scheduleProviderId);
|
setSelectedProviderIdRaw(scheduleProviderId);
|
||||||
setSelectedModelRaw(
|
setSelectedModelRaw(
|
||||||
scheduleProviderId === normalizeProviderForMode(schedule.launchConfig.providerId, true)
|
schedule.launchConfig.providerId !== 'gemini' &&
|
||||||
|
scheduleProviderId === normalizeProviderForMode(schedule.launchConfig.providerId, true)
|
||||||
? (schedule.launchConfig.model ?? '')
|
? (schedule.launchConfig.model ?? '')
|
||||||
: getStoredTeamModel('anthropic')
|
: getStoredTeamModel('anthropic')
|
||||||
);
|
);
|
||||||
|
|
@ -576,6 +587,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
);
|
);
|
||||||
setSelectedModelRaw(
|
setSelectedModelRaw(
|
||||||
typeof savedRequest?.model === 'string' &&
|
typeof savedRequest?.model === 'string' &&
|
||||||
|
rawNextProviderId !== 'gemini' &&
|
||||||
nextProviderId === normalizeProviderForMode(rawNextProviderId, true)
|
nextProviderId === normalizeProviderForMode(rawNextProviderId, true)
|
||||||
? savedRequest.model
|
? savedRequest.model
|
||||||
: getStoredTeamModel(
|
: getStoredTeamModel(
|
||||||
|
|
@ -1539,6 +1551,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
leadWarningText={leadRuntimeWarningText}
|
leadWarningText={leadRuntimeWarningText}
|
||||||
memberWarningById={memberRuntimeWarningById}
|
memberWarningById={memberRuntimeWarningById}
|
||||||
softDeleteMembers
|
softDeleteMembers
|
||||||
|
disableGeminiOption={isGeminiUiFrozen()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
|
|
@ -1678,6 +1691,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onValueChange={setSelectedModel}
|
onValueChange={setSelectedModel}
|
||||||
id="dialog-model"
|
id="dialog-model"
|
||||||
|
disableGeminiOption={isGeminiUiFrozen()}
|
||||||
/>
|
/>
|
||||||
<EffortLevelSelector
|
<EffortLevelSelector
|
||||||
value={selectedEffort}
|
value={selectedEffort}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,16 @@ import {
|
||||||
} from '@renderer/components/ui/tooltip';
|
} from '@renderer/components/ui/tooltip';
|
||||||
import { cn } from '@renderer/lib/utils';
|
import { cn } from '@renderer/lib/utils';
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
|
import {
|
||||||
|
GEMINI_UI_DISABLED_BADGE_LABEL,
|
||||||
|
GEMINI_UI_DISABLED_REASON,
|
||||||
|
isGeminiUiFrozen,
|
||||||
|
} from '@renderer/utils/geminiUiFreeze';
|
||||||
|
import {
|
||||||
|
getTeamModelUiDisabledReason,
|
||||||
|
normalizeTeamModelForUi,
|
||||||
|
TEAM_MODEL_UI_DISABLED_BADGE_LABEL,
|
||||||
|
} from '@renderer/utils/teamModelAvailability';
|
||||||
import { Check, ChevronDown, Info } from 'lucide-react';
|
import { Check, ChevronDown, Info } from 'lucide-react';
|
||||||
|
|
||||||
// --- Provider SVG Icons (real brand logos from Simple Icons, monochrome currentColor) ---
|
// --- Provider SVG Icons (real brand logos from Simple Icons, monochrome currentColor) ---
|
||||||
|
|
@ -45,6 +55,7 @@ interface ProviderDef {
|
||||||
const PROVIDERS: ProviderDef[] = [
|
const PROVIDERS: ProviderDef[] = [
|
||||||
{ id: 'anthropic', label: 'Anthropic', icon: AnthropicIcon, comingSoon: false },
|
{ id: 'anthropic', label: 'Anthropic', icon: AnthropicIcon, comingSoon: false },
|
||||||
{ id: 'codex', label: 'Codex', icon: OpenAIIcon, comingSoon: false },
|
{ id: 'codex', label: 'Codex', icon: OpenAIIcon, comingSoon: false },
|
||||||
|
// { id: 'gemini', label: 'Gemini', icon: GoogleGeminiIcon, comingSoon: false },
|
||||||
{ id: 'gemini', label: 'Gemini', icon: GoogleGeminiIcon, comingSoon: false },
|
{ id: 'gemini', label: 'Gemini', icon: GoogleGeminiIcon, comingSoon: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -170,6 +181,7 @@ export interface TeamModelSelectorProps {
|
||||||
value: string;
|
value: string;
|
||||||
onValueChange: (value: string) => void;
|
onValueChange: (value: string) => void;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
disableGeminiOption?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
|
|
@ -178,6 +190,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
value,
|
value,
|
||||||
onValueChange,
|
onValueChange,
|
||||||
id,
|
id,
|
||||||
|
disableGeminiOption = false,
|
||||||
}) => {
|
}) => {
|
||||||
const cliStatus = useStore((s) => s.cliStatus);
|
const cliStatus = useStore((s) => s.cliStatus);
|
||||||
const multimodelEnabled = useStore((s) => s.appConfig?.general?.multimodelEnabled ?? true);
|
const multimodelEnabled = useStore((s) => s.appConfig?.general?.multimodelEnabled ?? true);
|
||||||
|
|
@ -199,27 +212,42 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
}, [dropdownOpen]);
|
}, [dropdownOpen]);
|
||||||
|
|
||||||
const activeProvider = PROVIDERS.find((provider) => provider.id === providerId) ?? PROVIDERS[0];
|
const effectiveProviderId =
|
||||||
|
disableGeminiOption && isGeminiUiFrozen() && providerId === 'gemini' ? 'anthropic' : providerId;
|
||||||
|
const activeProvider =
|
||||||
|
PROVIDERS.find((provider) => provider.id === effectiveProviderId) ?? PROVIDERS[0];
|
||||||
const ProviderIcon = activeProvider.icon;
|
const ProviderIcon = activeProvider.icon;
|
||||||
const defaultModelTooltip = useMemo(() => {
|
const defaultModelTooltip = useMemo(() => {
|
||||||
if (providerId === 'anthropic') {
|
if (effectiveProviderId === 'anthropic') {
|
||||||
return 'Default model from Claude CLI (/model).\nUses the runtime default for the selected provider.';
|
return 'Default model from Claude CLI (/model).\nUses the runtime default for the selected provider.';
|
||||||
}
|
}
|
||||||
return 'Uses the runtime default for the selected provider.';
|
return 'Uses the runtime default for the selected provider.';
|
||||||
}, [providerId]);
|
}, [effectiveProviderId]);
|
||||||
|
const isProviderTemporarilyDisabled = (candidateProviderId: string): boolean =>
|
||||||
|
disableGeminiOption && isGeminiUiFrozen() && candidateProviderId === 'gemini';
|
||||||
const isProviderSelectable = (candidateProviderId: string): boolean =>
|
const isProviderSelectable = (candidateProviderId: string): boolean =>
|
||||||
multimodelAvailable || candidateProviderId === 'anthropic';
|
!isProviderTemporarilyDisabled(candidateProviderId) &&
|
||||||
const activeProviderSelectable = isProviderSelectable(providerId);
|
(multimodelAvailable || candidateProviderId === 'anthropic');
|
||||||
|
const activeProviderSelectable = isProviderSelectable(effectiveProviderId);
|
||||||
const runtimeModels =
|
const runtimeModels =
|
||||||
cliStatus?.providers.find((provider) => provider.providerId === providerId)?.models ?? [];
|
cliStatus?.providers.find((provider) => provider.providerId === effectiveProviderId)?.models ??
|
||||||
|
[];
|
||||||
|
const normalizedValue = normalizeTeamModelForUi(effectiveProviderId, value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (normalizedValue !== value) {
|
||||||
|
onValueChange(normalizedValue);
|
||||||
|
}
|
||||||
|
}, [normalizedValue, onValueChange, value]);
|
||||||
|
|
||||||
const modelOptions = useMemo(() => {
|
const modelOptions = useMemo(() => {
|
||||||
const fallback =
|
const fallback =
|
||||||
providerId === 'codex'
|
effectiveProviderId === 'codex'
|
||||||
? CODEX_MODEL_OPTIONS
|
? CODEX_MODEL_OPTIONS
|
||||||
: providerId === 'gemini'
|
: effectiveProviderId === 'gemini'
|
||||||
? GEMINI_MODEL_OPTIONS
|
? GEMINI_MODEL_OPTIONS
|
||||||
: ANTHROPIC_MODEL_OPTIONS;
|
: ANTHROPIC_MODEL_OPTIONS;
|
||||||
if (providerId === 'anthropic' || runtimeModels.length === 0) {
|
if (effectiveProviderId === 'anthropic' || runtimeModels.length === 0) {
|
||||||
return [...fallback];
|
return [...fallback];
|
||||||
}
|
}
|
||||||
const dynamicOptions = runtimeModels.map((model) => ({
|
const dynamicOptions = runtimeModels.map((model) => ({
|
||||||
|
|
@ -227,7 +255,7 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
label: getTeamModelLabel(model),
|
label: getTeamModelLabel(model),
|
||||||
}));
|
}));
|
||||||
return [{ value: '', label: 'Default' }, ...dynamicOptions];
|
return [{ value: '', label: 'Default' }, ...dynamicOptions];
|
||||||
}, [providerId, runtimeModels]);
|
}, [effectiveProviderId, runtimeModels]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
|
|
@ -317,11 +345,21 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
Coming Soon
|
Coming Soon
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!provider.comingSoon && !isProviderSelectable(provider.id) && (
|
{!provider.comingSoon && isProviderTemporarilyDisabled(provider.id) && (
|
||||||
<span className="rounded bg-white/5 px-1.5 py-0.5 text-[10px] text-[var(--color-text-muted)]">
|
<span
|
||||||
Multimodel off
|
className="rounded bg-white/5 px-1.5 py-0.5 text-[10px] text-[var(--color-text-muted)]"
|
||||||
|
title={GEMINI_UI_DISABLED_REASON}
|
||||||
|
>
|
||||||
|
{GEMINI_UI_DISABLED_BADGE_LABEL}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{!provider.comingSoon &&
|
||||||
|
!isProviderTemporarilyDisabled(provider.id) &&
|
||||||
|
!isProviderSelectable(provider.id) && (
|
||||||
|
<span className="rounded bg-white/5 px-1.5 py-0.5 text-[10px] text-[var(--color-text-muted)]">
|
||||||
|
Multimodel off
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{isActive && <Check className="size-3.5 shrink-0" />}
|
{isActive && <Check className="size-3.5 shrink-0" />}
|
||||||
</button>
|
</button>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
@ -335,52 +373,97 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
Codex and Gemini require Multimodel mode.
|
Codex and Gemini require Multimodel mode.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{disableGeminiOption && isGeminiUiFrozen() && (
|
||||||
|
<p className="text-[11px] text-[var(--color-text-muted)]">{GEMINI_UI_DISABLED_REASON}.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="grid gap-1.5 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1.5"
|
className="grid gap-1.5 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-1.5"
|
||||||
style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))' }}
|
style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))' }}
|
||||||
>
|
>
|
||||||
{modelOptions.map((opt) => (
|
{modelOptions.map((opt) =>
|
||||||
<button
|
(() => {
|
||||||
key={opt.value || '__default__'}
|
const modelDisabledReason = getTeamModelUiDisabledReason(
|
||||||
type="button"
|
effectiveProviderId,
|
||||||
id={opt.value === value ? id : undefined}
|
opt.value
|
||||||
className={cn(
|
);
|
||||||
'flex min-h-[44px] items-center justify-center gap-1.5 rounded-md border px-3 py-2 text-center text-xs font-medium transition-colors',
|
const modelSelectable = activeProviderSelectable && !modelDisabledReason;
|
||||||
value === opt.value
|
|
||||||
? 'bg-[var(--color-surface-raised)] text-[var(--color-text)] shadow-sm'
|
return (
|
||||||
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]',
|
<button
|
||||||
!activeProviderSelectable && 'cursor-not-allowed opacity-45'
|
key={opt.value || '__default__'}
|
||||||
)}
|
type="button"
|
||||||
style={{
|
id={opt.value === normalizedValue ? id : undefined}
|
||||||
borderColor: value === opt.value ? 'var(--color-border-emphasis)' : 'transparent',
|
aria-disabled={!modelSelectable}
|
||||||
}}
|
className={cn(
|
||||||
disabled={!activeProviderSelectable}
|
'flex min-h-[44px] items-center justify-center gap-1.5 rounded-md border px-3 py-2 text-center text-xs font-medium transition-colors',
|
||||||
onClick={() => {
|
normalizedValue === opt.value
|
||||||
if (!activeProviderSelectable) return;
|
? 'bg-[var(--color-surface-raised)] text-[var(--color-text)] shadow-sm'
|
||||||
onValueChange(opt.value);
|
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]',
|
||||||
}}
|
!modelSelectable && 'cursor-not-allowed opacity-45',
|
||||||
>
|
!modelDisabledReason && !activeProviderSelectable && 'pointer-events-none'
|
||||||
<span className="leading-tight">{opt.label}</span>
|
)}
|
||||||
{opt.value === '' && (
|
style={{
|
||||||
<TooltipProvider delayDuration={200}>
|
borderColor:
|
||||||
<Tooltip>
|
normalizedValue === opt.value
|
||||||
<TooltipTrigger asChild onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
? 'var(--color-border-emphasis)'
|
||||||
<Info className="size-3 shrink-0 opacity-40 transition-opacity hover:opacity-70" />
|
: 'transparent',
|
||||||
</TooltipTrigger>
|
}}
|
||||||
<TooltipContent side="top" className="max-w-[240px] text-xs">
|
onClick={() => {
|
||||||
{defaultModelTooltip.split('\n').map((line, index) => (
|
if (!modelSelectable) return;
|
||||||
<React.Fragment key={line}>
|
onValueChange(opt.value);
|
||||||
{index > 0 ? <br /> : null}
|
}}
|
||||||
{line}
|
>
|
||||||
</React.Fragment>
|
<span className="flex flex-col items-center justify-center gap-0.5">
|
||||||
))}
|
<span className="leading-tight">{opt.label}</span>
|
||||||
</TooltipContent>
|
{opt.value === '' && (
|
||||||
</Tooltip>
|
<span className="flex items-center justify-center gap-1">
|
||||||
</TooltipProvider>
|
<TooltipProvider delayDuration={200}>
|
||||||
)}
|
<Tooltip>
|
||||||
</button>
|
<TooltipTrigger
|
||||||
))}
|
asChild
|
||||||
|
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Info className="size-3 shrink-0 opacity-40 transition-opacity hover:opacity-70" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="max-w-[240px] text-xs">
|
||||||
|
{defaultModelTooltip.split('\n').map((line, index) => (
|
||||||
|
<React.Fragment key={line}>
|
||||||
|
{index > 0 ? <br /> : null}
|
||||||
|
{line}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{modelDisabledReason && (
|
||||||
|
<span
|
||||||
|
className="flex items-center justify-center gap-1 text-[10px] font-normal text-[var(--color-text-muted)]"
|
||||||
|
title={modelDisabledReason}
|
||||||
|
>
|
||||||
|
<span>{TEAM_MODEL_UI_DISABLED_BADGE_LABEL}</span>
|
||||||
|
<TooltipProvider delayDuration={200}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger
|
||||||
|
asChild
|
||||||
|
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Info className="size-3 shrink-0 opacity-40 transition-opacity hover:opacity-70" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="max-w-[240px] text-xs">
|
||||||
|
{modelDisabledReason}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ interface LeadModelRowProps {
|
||||||
syncModelsWithTeammates: boolean;
|
syncModelsWithTeammates: boolean;
|
||||||
onSyncModelsWithTeammatesChange: (value: boolean) => void;
|
onSyncModelsWithTeammatesChange: (value: boolean) => void;
|
||||||
warningText?: string | null;
|
warningText?: string | null;
|
||||||
|
disableGeminiOption?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LeadModelRow = ({
|
export const LeadModelRow = ({
|
||||||
|
|
@ -43,6 +44,7 @@ export const LeadModelRow = ({
|
||||||
syncModelsWithTeammates,
|
syncModelsWithTeammates,
|
||||||
onSyncModelsWithTeammatesChange,
|
onSyncModelsWithTeammatesChange,
|
||||||
warningText,
|
warningText,
|
||||||
|
disableGeminiOption = false,
|
||||||
}: LeadModelRowProps): React.JSX.Element => {
|
}: LeadModelRowProps): React.JSX.Element => {
|
||||||
const { isLight } = useTheme();
|
const { isLight } = useTheme();
|
||||||
const [modelExpanded, setModelExpanded] = useState(false);
|
const [modelExpanded, setModelExpanded] = useState(false);
|
||||||
|
|
@ -120,6 +122,7 @@ export const LeadModelRow = ({
|
||||||
value={model}
|
value={model}
|
||||||
onValueChange={onModelChange}
|
onValueChange={onModelChange}
|
||||||
id="lead-model"
|
id="lead-model"
|
||||||
|
disableGeminiOption={disableGeminiOption}
|
||||||
/>
|
/>
|
||||||
<EffortLevelSelector
|
<EffortLevelSelector
|
||||||
value={effort ?? ''}
|
value={effort ?? ''}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ interface MemberDraftRowProps {
|
||||||
isRemoved?: boolean;
|
isRemoved?: boolean;
|
||||||
onRestore?: (id: string) => void;
|
onRestore?: (id: string) => void;
|
||||||
warningText?: string | null;
|
warningText?: string | null;
|
||||||
|
disableGeminiOption?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MemberDraftRow = ({
|
export const MemberDraftRow = ({
|
||||||
|
|
@ -83,6 +84,7 @@ export const MemberDraftRow = ({
|
||||||
isRemoved = false,
|
isRemoved = false,
|
||||||
onRestore,
|
onRestore,
|
||||||
warningText,
|
warningText,
|
||||||
|
disableGeminiOption = false,
|
||||||
}: MemberDraftRowProps): React.JSX.Element => {
|
}: MemberDraftRowProps): React.JSX.Element => {
|
||||||
const { isLight } = useTheme();
|
const { isLight } = useTheme();
|
||||||
const memberColorSet = getTeamColorSet(
|
const memberColorSet = getTeamColorSet(
|
||||||
|
|
@ -344,6 +346,7 @@ export const MemberDraftRow = ({
|
||||||
onModelChange(member.id, value);
|
onModelChange(member.id, value);
|
||||||
}}
|
}}
|
||||||
id={`member-${member.id}-model`}
|
id={`member-${member.id}-model`}
|
||||||
|
disableGeminiOption={disableGeminiOption}
|
||||||
/>
|
/>
|
||||||
<EffortLevelSelector
|
<EffortLevelSelector
|
||||||
value={effectiveEffort ?? ''}
|
value={effectiveEffort ?? ''}
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ export interface MembersEditorSectionProps {
|
||||||
modelLockReason?: string;
|
modelLockReason?: string;
|
||||||
softDeleteMembers?: boolean;
|
softDeleteMembers?: boolean;
|
||||||
memberWarningById?: Record<string, string | null | undefined>;
|
memberWarningById?: Record<string, string | null | undefined>;
|
||||||
|
disableGeminiOption?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MembersEditorSection = ({
|
export const MembersEditorSection = ({
|
||||||
|
|
@ -126,6 +127,7 @@ export const MembersEditorSection = ({
|
||||||
modelLockReason,
|
modelLockReason,
|
||||||
softDeleteMembers = false,
|
softDeleteMembers = false,
|
||||||
memberWarningById,
|
memberWarningById,
|
||||||
|
disableGeminiOption = false,
|
||||||
}: MembersEditorSectionProps): React.JSX.Element => {
|
}: MembersEditorSectionProps): React.JSX.Element => {
|
||||||
const [jsonEditorOpen, setJsonEditorOpen] = useState(false);
|
const [jsonEditorOpen, setJsonEditorOpen] = useState(false);
|
||||||
const [jsonText, setJsonText] = useState('');
|
const [jsonText, setJsonText] = useState('');
|
||||||
|
|
@ -313,6 +315,7 @@ export const MembersEditorSection = ({
|
||||||
lockProviderModel={lockProviderModel}
|
lockProviderModel={lockProviderModel}
|
||||||
modelLockReason={modelLockReason}
|
modelLockReason={modelLockReason}
|
||||||
warningText={memberWarningById?.[member.id] ?? null}
|
warningText={memberWarningById?.[member.id] ?? null}
|
||||||
|
disableGeminiOption={disableGeminiOption}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{softDeleteMembers && removedMembers.length > 0 ? (
|
{softDeleteMembers && removedMembers.length > 0 ? (
|
||||||
|
|
@ -352,6 +355,7 @@ export const MembersEditorSection = ({
|
||||||
modelLockReason="Removed members are kept for soft delete history. Restore them to edit settings."
|
modelLockReason="Removed members are kept for soft delete history. Restore them to edit settings."
|
||||||
isRemoved
|
isRemoved
|
||||||
warningText={null}
|
warningText={null}
|
||||||
|
disableGeminiOption={disableGeminiOption}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ interface TeamRosterEditorSectionProps {
|
||||||
softDeleteMembers?: boolean;
|
softDeleteMembers?: boolean;
|
||||||
leadWarningText?: string | null;
|
leadWarningText?: string | null;
|
||||||
memberWarningById?: Record<string, string | null | undefined>;
|
memberWarningById?: Record<string, string | null | undefined>;
|
||||||
|
disableGeminiOption?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TeamRosterEditorSection = ({
|
export const TeamRosterEditorSection = ({
|
||||||
|
|
@ -81,6 +82,7 @@ export const TeamRosterEditorSection = ({
|
||||||
softDeleteMembers = false,
|
softDeleteMembers = false,
|
||||||
leadWarningText,
|
leadWarningText,
|
||||||
memberWarningById,
|
memberWarningById,
|
||||||
|
disableGeminiOption = false,
|
||||||
}: TeamRosterEditorSectionProps): React.JSX.Element => {
|
}: TeamRosterEditorSectionProps): React.JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<MembersEditorSection
|
<MembersEditorSection
|
||||||
|
|
@ -105,6 +107,7 @@ export const TeamRosterEditorSection = ({
|
||||||
forceInheritedModelSettings={forceInheritedModelSettings}
|
forceInheritedModelSettings={forceInheritedModelSettings}
|
||||||
modelLockReason={modelLockReason}
|
modelLockReason={modelLockReason}
|
||||||
softDeleteMembers={softDeleteMembers}
|
softDeleteMembers={softDeleteMembers}
|
||||||
|
disableGeminiOption={disableGeminiOption}
|
||||||
headerExtra={
|
headerExtra={
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{headerTop}
|
{headerTop}
|
||||||
|
|
@ -120,6 +123,7 @@ export const TeamRosterEditorSection = ({
|
||||||
syncModelsWithTeammates={syncModelsWithTeammates}
|
syncModelsWithTeammates={syncModelsWithTeammates}
|
||||||
onSyncModelsWithTeammatesChange={onSyncModelsWithTeammatesChange}
|
onSyncModelsWithTeammatesChange={onSyncModelsWithTeammatesChange}
|
||||||
warningText={leadWarningText}
|
warningText={leadWarningText}
|
||||||
|
disableGeminiOption={disableGeminiOption}
|
||||||
/>
|
/>
|
||||||
{headerBottom}
|
{headerBottom}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { CUSTOM_ROLE, NO_ROLE, PRESET_ROLES } from '@renderer/constants/teamRoles';
|
import { CUSTOM_ROLE, NO_ROLE, PRESET_ROLES } from '@renderer/constants/teamRoles';
|
||||||
|
import { normalizeCreateLaunchProviderForUi } from '@renderer/utils/geminiUiFreeze';
|
||||||
|
import { normalizeTeamModelForUi } from '@renderer/utils/teamModelAvailability';
|
||||||
import { serializeChipsWithText } from '@renderer/types/inlineChip';
|
import { serializeChipsWithText } from '@renderer/types/inlineChip';
|
||||||
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
|
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
|
||||||
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
||||||
|
|
@ -28,14 +30,15 @@ function newDraftId(): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMemberDraft(initial?: Partial<MemberDraft>): MemberDraft {
|
export function createMemberDraft(initial?: Partial<MemberDraft>): MemberDraft {
|
||||||
|
const providerId = initial?.providerId;
|
||||||
return {
|
return {
|
||||||
id: initial?.id ?? newDraftId(),
|
id: initial?.id ?? newDraftId(),
|
||||||
name: initial?.name ?? '',
|
name: initial?.name ?? '',
|
||||||
roleSelection: initial?.roleSelection ?? '',
|
roleSelection: initial?.roleSelection ?? '',
|
||||||
customRole: initial?.customRole ?? '',
|
customRole: initial?.customRole ?? '',
|
||||||
workflow: initial?.workflow,
|
workflow: initial?.workflow,
|
||||||
providerId: initial?.providerId,
|
providerId,
|
||||||
model: initial?.model ?? '',
|
model: normalizeTeamModelForUi(providerId, initial?.model ?? ''),
|
||||||
effort: initial?.effort,
|
effort: initial?.effort,
|
||||||
removedAt: initial?.removedAt,
|
removedAt: initial?.removedAt,
|
||||||
};
|
};
|
||||||
|
|
@ -84,23 +87,30 @@ export function normalizeProviderForMode(
|
||||||
providerId: TeamProviderId | undefined,
|
providerId: TeamProviderId | undefined,
|
||||||
multimodelEnabled: boolean
|
multimodelEnabled: boolean
|
||||||
): TeamProviderId {
|
): TeamProviderId {
|
||||||
if (multimodelEnabled && (providerId === 'codex' || providerId === 'gemini')) {
|
return normalizeCreateLaunchProviderForUi(providerId, multimodelEnabled);
|
||||||
return providerId;
|
|
||||||
}
|
|
||||||
return 'anthropic';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeMemberDraftForProviderMode(
|
export function normalizeMemberDraftForProviderMode(
|
||||||
member: MemberDraft,
|
member: MemberDraft,
|
||||||
multimodelEnabled: boolean
|
multimodelEnabled: boolean
|
||||||
): MemberDraft {
|
): MemberDraft {
|
||||||
if (multimodelEnabled) {
|
const normalizedProviderId =
|
||||||
|
member.providerId == null
|
||||||
|
? undefined
|
||||||
|
: normalizeCreateLaunchProviderForUi(member.providerId, multimodelEnabled);
|
||||||
|
|
||||||
|
if (normalizedProviderId === member.providerId) {
|
||||||
return member;
|
return member;
|
||||||
}
|
}
|
||||||
if (member.providerId === 'codex' || member.providerId === 'gemini') {
|
|
||||||
|
if (
|
||||||
|
member.providerId === 'codex' ||
|
||||||
|
member.providerId === 'gemini' ||
|
||||||
|
normalizedProviderId !== member.providerId
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
...member,
|
...member,
|
||||||
providerId: 'anthropic',
|
providerId: normalizedProviderId,
|
||||||
model: '',
|
model: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -200,7 +210,7 @@ export function buildMembersFromDrafts(members: MemberDraft[]): TeamProvisioning
|
||||||
}
|
}
|
||||||
const model = member.model?.trim();
|
const model = member.model?.trim();
|
||||||
if (model) {
|
if (model) {
|
||||||
result.model = model;
|
result.model = normalizeTeamModelForUi(providerId, model);
|
||||||
}
|
}
|
||||||
const effort = normalizeDraftEffort(member.effort);
|
const effort = normalizeDraftEffort(member.effort);
|
||||||
if (effort) {
|
if (effort) {
|
||||||
|
|
|
||||||
57
src/renderer/utils/geminiUiFreeze.ts
Normal file
57
src/renderer/utils/geminiUiFreeze.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import type { CliProviderId } from '@shared/types/cliInstaller';
|
||||||
|
import type { TeamProviderId } from '@shared/types';
|
||||||
|
|
||||||
|
export const GEMINI_UI_FROZEN = true;
|
||||||
|
export const GEMINI_UI_DISABLED_REASON = 'Gemini in development';
|
||||||
|
export const GEMINI_UI_DISABLED_BADGE_LABEL = 'In development';
|
||||||
|
|
||||||
|
export function isGeminiUiFrozen(): boolean {
|
||||||
|
return GEMINI_UI_FROZEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGeminiProviderId(
|
||||||
|
providerId: CliProviderId | TeamProviderId | undefined
|
||||||
|
): providerId is 'gemini' {
|
||||||
|
return providerId === 'gemini';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterMainScreenCliProviders<T extends { providerId: CliProviderId }>(
|
||||||
|
providers: readonly T[]
|
||||||
|
): T[] {
|
||||||
|
if (!GEMINI_UI_FROZEN) {
|
||||||
|
return [...providers];
|
||||||
|
}
|
||||||
|
|
||||||
|
return providers.filter((provider) => provider.providerId !== 'gemini');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeCreateLaunchProviderForUi(
|
||||||
|
providerId: TeamProviderId | undefined,
|
||||||
|
multimodelEnabled: boolean
|
||||||
|
): TeamProviderId {
|
||||||
|
if (!multimodelEnabled) {
|
||||||
|
return 'anthropic';
|
||||||
|
}
|
||||||
|
|
||||||
|
// return providerId === 'codex' || providerId === 'gemini' ? providerId : 'anthropic';
|
||||||
|
if (providerId === 'codex') {
|
||||||
|
return 'codex';
|
||||||
|
}
|
||||||
|
if (providerId === 'gemini' && GEMINI_UI_FROZEN) {
|
||||||
|
return 'anthropic';
|
||||||
|
}
|
||||||
|
return providerId === 'anthropic' ? 'anthropic' : 'anthropic';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCreateLaunchProviderDisabled(
|
||||||
|
providerId: TeamProviderId,
|
||||||
|
multimodelEnabled: boolean
|
||||||
|
): boolean {
|
||||||
|
if (providerId === 'gemini' && GEMINI_UI_FROZEN) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!multimodelEnabled && providerId !== 'anthropic') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
30
src/renderer/utils/teamModelAvailability.ts
Normal file
30
src/renderer/utils/teamModelAvailability.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import type { TeamProviderId } from '@shared/types';
|
||||||
|
|
||||||
|
export const TEAM_MODEL_UI_DISABLED_BADGE_LABEL = 'Disabled';
|
||||||
|
export const GPT_5_1_CODEX_MINI_UI_DISABLED_MODEL = 'gpt-5.1-codex-mini';
|
||||||
|
export const GPT_5_1_CODEX_MINI_UI_DISABLED_REASON =
|
||||||
|
'Temporarily disabled for team agents - this model has been less reliable with task and reply tool contracts.';
|
||||||
|
|
||||||
|
export function getTeamModelUiDisabledReason(
|
||||||
|
providerId: TeamProviderId | undefined,
|
||||||
|
model: string | undefined
|
||||||
|
): string | null {
|
||||||
|
if (providerId === 'codex' && model === GPT_5_1_CODEX_MINI_UI_DISABLED_MODEL) {
|
||||||
|
return GPT_5_1_CODEX_MINI_UI_DISABLED_REASON;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTeamModelUiDisabled(
|
||||||
|
providerId: TeamProviderId | undefined,
|
||||||
|
model: string | undefined
|
||||||
|
): boolean {
|
||||||
|
return getTeamModelUiDisabledReason(providerId, model) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeTeamModelForUi(
|
||||||
|
providerId: TeamProviderId | undefined,
|
||||||
|
model: string | undefined
|
||||||
|
): string {
|
||||||
|
return isTeamModelUiDisabled(providerId, model) ? '' : (model ?? '');
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { formatTeamModelSummary } from '@renderer/components/team/dialogs/TeamModelSelector';
|
import { formatTeamModelSummary } from '@renderer/components/team/dialogs/TeamModelSelector';
|
||||||
|
import {
|
||||||
|
GPT_5_1_CODEX_MINI_UI_DISABLED_REASON,
|
||||||
|
getTeamModelUiDisabledReason,
|
||||||
|
normalizeTeamModelForUi,
|
||||||
|
} from '@renderer/utils/teamModelAvailability';
|
||||||
|
|
||||||
describe('formatTeamModelSummary', () => {
|
describe('formatTeamModelSummary', () => {
|
||||||
it('shows cross-provider Anthropic models as backend-routed instead of brand-mismatched', () => {
|
it('shows cross-provider Anthropic models as backend-routed instead of brand-mismatched', () => {
|
||||||
|
|
@ -12,4 +17,17 @@ describe('formatTeamModelSummary', () => {
|
||||||
it('keeps native Codex-family models branded normally', () => {
|
it('keeps native Codex-family models branded normally', () => {
|
||||||
expect(formatTeamModelSummary('codex', 'gpt-5.4', 'medium')).toBe('GPT-5.4 · Medium');
|
expect(formatTeamModelSummary('codex', 'gpt-5.4', 'medium')).toBe('GPT-5.4 · Medium');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('marks GPT-5.1 Codex Mini as disabled only for Codex team selection', () => {
|
||||||
|
expect(getTeamModelUiDisabledReason('codex', 'gpt-5.1-codex-mini')).toBe(
|
||||||
|
GPT_5_1_CODEX_MINI_UI_DISABLED_REASON
|
||||||
|
);
|
||||||
|
expect(getTeamModelUiDisabledReason('codex', 'gpt-5.4-mini')).toBeNull();
|
||||||
|
expect(getTeamModelUiDisabledReason('anthropic', 'gpt-5.1-codex-mini')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes disabled Codex model selections back to default', () => {
|
||||||
|
expect(normalizeTeamModelForUi('codex', 'gpt-5.1-codex-mini')).toBe('');
|
||||||
|
expect(normalizeTeamModelForUi('codex', 'gpt-5.4-mini')).toBe('gpt-5.4-mini');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
import React, { act } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
vi.mock('@renderer/components/ui/tooltip', () => ({
|
||||||
|
TooltipProvider: ({ children }: { children: React.ReactNode }) =>
|
||||||
|
React.createElement(React.Fragment, null, children),
|
||||||
|
Tooltip: ({ children }: { children: React.ReactNode }) =>
|
||||||
|
React.createElement(React.Fragment, null, children),
|
||||||
|
TooltipTrigger: ({ children }: { children: React.ReactNode }) =>
|
||||||
|
React.createElement(React.Fragment, null, children),
|
||||||
|
TooltipContent: ({ children }: { children: React.ReactNode }) =>
|
||||||
|
React.createElement('div', null, children),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@renderer/store', () => ({
|
||||||
|
useStore: (selector: (state: unknown) => unknown) =>
|
||||||
|
selector({
|
||||||
|
cliStatus: null,
|
||||||
|
appConfig: { general: { multimodelEnabled: true } },
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { TeamModelSelector } from '@renderer/components/team/dialogs/TeamModelSelector';
|
||||||
|
import { GPT_5_1_CODEX_MINI_UI_DISABLED_REASON } from '@renderer/utils/teamModelAvailability';
|
||||||
|
|
||||||
|
describe('TeamModelSelector disabled Codex models', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders GPT-5.1 Codex Mini as disabled with an explanation tooltip', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(
|
||||||
|
React.createElement(TeamModelSelector, {
|
||||||
|
providerId: 'codex',
|
||||||
|
onProviderChange: () => undefined,
|
||||||
|
value: '',
|
||||||
|
onValueChange: () => undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.textContent).toContain('GPT-5.1 Codex Mini');
|
||||||
|
expect(host.textContent).toContain('Disabled');
|
||||||
|
expect(host.textContent).toContain(GPT_5_1_CODEX_MINI_UI_DISABLED_REASON);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes a stale disabled selection back to default', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
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: 'gpt-5.1-codex-mini',
|
||||||
|
onValueChange,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onValueChange).toHaveBeenCalledWith('');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
29
test/renderer/utils/geminiUiFreeze.test.ts
Normal file
29
test/renderer/utils/geminiUiFreeze.test.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
filterMainScreenCliProviders,
|
||||||
|
normalizeCreateLaunchProviderForUi,
|
||||||
|
} from '@renderer/utils/geminiUiFreeze';
|
||||||
|
|
||||||
|
describe('geminiUiFreeze', () => {
|
||||||
|
it('hides gemini from the dashboard provider list', () => {
|
||||||
|
expect(
|
||||||
|
filterMainScreenCliProviders([
|
||||||
|
{ providerId: 'anthropic', label: 'Anthropic' },
|
||||||
|
{ providerId: 'codex', label: 'Codex' },
|
||||||
|
{ providerId: 'gemini', label: 'Gemini' },
|
||||||
|
])
|
||||||
|
).toEqual([
|
||||||
|
{ providerId: 'anthropic', label: 'Anthropic' },
|
||||||
|
{ providerId: 'codex', label: 'Codex' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to anthropic when a create or launch form receives gemini', () => {
|
||||||
|
expect(normalizeCreateLaunchProviderForUi('gemini', true)).toBe('anthropic');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps codex available when multimodel is enabled', () => {
|
||||||
|
expect(normalizeCreateLaunchProviderForUi('codex', true)).toBe('codex');
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue