merge main to pick up German locale for custom provider keys
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
@@ -23,9 +23,17 @@ interface ModelSelectorProps {
|
||||
onChange: (providerId: string, modelId: string) => void;
|
||||
className?: string;
|
||||
allowedProviderIds?: string[];
|
||||
isModelAllowed?: (providerId: string, modelId: string) => boolean;
|
||||
placeholder?: string;
|
||||
tooltipsEnabled?: boolean;
|
||||
dropdownPortalToBody?: boolean;
|
||||
/**
|
||||
* Drop the model name and the chevron, leaving the provider logo. For
|
||||
* headers that run out of room before they run out of controls — the logo
|
||||
* still says which provider is answering, which is the part a glance is
|
||||
* usually after.
|
||||
*/
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||
@@ -34,9 +42,11 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||
onChange,
|
||||
className,
|
||||
allowedProviderIds,
|
||||
isModelAllowed,
|
||||
placeholder,
|
||||
tooltipsEnabled = true,
|
||||
dropdownPortalToBody = false,
|
||||
compact = false,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const { isReady, isUnavailable } = useOpenCodeReadiness();
|
||||
@@ -115,6 +125,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||
selectedModel={selectedModel}
|
||||
hiddenModels={hiddenModels}
|
||||
allowedProviderIds={allowedProviderIds}
|
||||
isModelAllowed={isModelAllowed}
|
||||
includeNotSelected
|
||||
onSelectNone={handleSelectNone}
|
||||
onEscape={closePicker}
|
||||
@@ -166,26 +177,35 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
|
||||
return (
|
||||
<DropdownMenu open={isReady && isDropdownOpen} onOpenChange={isReady ? setIsDropdownOpen : undefined}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className={cn(
|
||||
dropdownTriggerVariants({ size: 'sm' }),
|
||||
'min-w-0 w-fit',
|
||||
!isReady && 'opacity-60 cursor-not-allowed',
|
||||
className,
|
||||
)}>
|
||||
<div
|
||||
className={cn(
|
||||
dropdownTriggerVariants({ size: 'sm' }),
|
||||
'min-w-0 w-fit',
|
||||
!isReady && 'opacity-60 cursor-not-allowed',
|
||||
className,
|
||||
)}
|
||||
// The name is gone from the trigger, so it has to stay
|
||||
// reachable somewhere.
|
||||
title={compact && isReady ? triggerLabel : undefined}
|
||||
>
|
||||
{!isReady ? (
|
||||
<>
|
||||
<Icon name="loader-4" className="h-3.5 w-3.5 animate-spin text-muted-foreground flex-shrink-0" />
|
||||
<span className="typography-ui-label font-normal whitespace-nowrap text-muted-foreground">
|
||||
{isUnavailable ? t('common.unavailable') : t('common.loading')}
|
||||
</span>
|
||||
{!compact && (
|
||||
<span className="typography-ui-label font-normal whitespace-nowrap text-muted-foreground">
|
||||
{isUnavailable ? t('common.unavailable') : t('common.loading')}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{providerId ? <ProviderLogo providerId={providerId} className="h-3.5 w-3.5 flex-shrink-0" /> : <Icon name="pencil-ai" className="h-3.5 w-3.5 flex-shrink-0 text-muted-foreground" />}
|
||||
<span className="typography-ui-label min-w-0 flex-1 truncate text-left font-normal text-foreground">{triggerLabel}</span>
|
||||
{!compact && (
|
||||
<span className="typography-ui-label min-w-0 flex-1 truncate text-left font-normal text-foreground">{triggerLabel}</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Icon name="arrow-down-s" className="h-4 w-4 flex-shrink-0 text-muted-foreground/50" />
|
||||
{!compact && <Icon name="arrow-down-s" className="h-4 w-4 flex-shrink-0 text-muted-foreground/50" />}
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-[min(380px,calc(100vw-2rem))] p-0 flex flex-col" align="start" portalToBody={dropdownPortalToBody}>
|
||||
|
||||
@@ -45,6 +45,7 @@ export const DefaultsSettings: React.FC = () => {
|
||||
const showDeletionDialog = useUIStore((state) => state.showDeletionDialog);
|
||||
const setShowDeletionDialog = useUIStore((state) => state.setShowDeletionDialog);
|
||||
const providers = useConfigStore((state) => state.providers);
|
||||
const modelsMetadata = useConfigStore((state) => state.modelsMetadata);
|
||||
|
||||
const [defaultModel, setDefaultModel] = React.useState<string | undefined>();
|
||||
const [defaultVariant, setDefaultVariant] = React.useState<string | undefined>();
|
||||
@@ -52,6 +53,7 @@ export const DefaultsSettings: React.FC = () => {
|
||||
const [smallModelUseDefault, setSmallModelUseDefault] = React.useState(true);
|
||||
const [smallModelOverride, setSmallModelOverride] = React.useState<string | undefined>();
|
||||
const [smallModelProviders, setSmallModelProviders] = React.useState<string[] | undefined>();
|
||||
const [walkthroughModelOverride, setWalkthroughModelOverride] = React.useState<string | undefined>();
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
|
||||
const parsedModel = React.useMemo(() => getDisplayModel(defaultModel), [defaultModel]);
|
||||
@@ -65,6 +67,7 @@ export const DefaultsSettings: React.FC = () => {
|
||||
defaultAgent?: string;
|
||||
smallModelUseDefault?: boolean;
|
||||
smallModelOverride?: string;
|
||||
walkthroughModelOverride?: string;
|
||||
} | null = null;
|
||||
|
||||
if (!data) {
|
||||
@@ -84,6 +87,8 @@ export const DefaultsSettings: React.FC = () => {
|
||||
defaultAgent: typeof settings.defaultAgent === 'string' ? settings.defaultAgent : undefined,
|
||||
smallModelUseDefault: typeof raw.smallModelUseDefault === 'boolean' ? raw.smallModelUseDefault : undefined,
|
||||
smallModelOverride: typeof raw.smallModelOverride === 'string' ? raw.smallModelOverride : undefined,
|
||||
walkthroughModelOverride:
|
||||
typeof raw.walkthroughModelOverride === 'string' ? raw.walkthroughModelOverride : undefined,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
@@ -123,6 +128,9 @@ export const DefaultsSettings: React.FC = () => {
|
||||
if (typeof data.smallModelOverride === 'string' && data.smallModelOverride.trim()) {
|
||||
setSmallModelOverride(data.smallModelOverride.trim());
|
||||
}
|
||||
if (typeof data.walkthroughModelOverride === 'string' && data.walkthroughModelOverride.trim()) {
|
||||
setWalkthroughModelOverride(data.walkthroughModelOverride.trim());
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load defaults settings:', error);
|
||||
@@ -236,10 +244,43 @@ export const DefaultsSettings: React.FC = () => {
|
||||
[]
|
||||
);
|
||||
|
||||
const handleWalkthroughModelOverrideChange = React.useCallback(
|
||||
async (providerId: string, modelId: string) => {
|
||||
const newValue = providerId && modelId ? `${providerId}/${modelId}` : undefined;
|
||||
setWalkthroughModelOverride(newValue);
|
||||
try {
|
||||
// Clearing the picker is how the user goes back to the small model, so
|
||||
// an empty value is a real choice rather than a no-op.
|
||||
await updateDesktopSettings({ walkthroughModelOverride: newValue ?? '' });
|
||||
} catch (error) {
|
||||
console.warn('Failed to save walkthrough model override:', error);
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// The walkthrough cannot work at all without schema-shaped output, so models
|
||||
// the catalog says cannot do it are hidden rather than offered and then
|
||||
// refused. A missing capability is not a "no": roughly half the catalog omits
|
||||
// the field, and those models usually work.
|
||||
const isStructuredOutputCapable = React.useCallback(
|
||||
(providerId: string, modelId: string) =>
|
||||
modelsMetadata.get(`${providerId}/${modelId}`)?.structured_output !== false,
|
||||
[modelsMetadata]
|
||||
);
|
||||
|
||||
const parsedSmallModel = React.useMemo(() => getDisplayModel(smallModelOverride), [smallModelOverride]);
|
||||
const parsedWalkthroughModel = React.useMemo(
|
||||
() => getDisplayModel(walkthroughModelOverride),
|
||||
[walkthroughModelOverride]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (smallModelUseDefault || smallModelProviders !== undefined) return;
|
||||
// Both pickers filter by the same authenticated-provider list, so either
|
||||
// one being open is reason enough to fetch it.
|
||||
// Both pickers filter by the same authenticated-provider list, and the
|
||||
// walkthrough picker is always visible, so this is always worth fetching.
|
||||
if (smallModelProviders !== undefined) return;
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
@@ -256,7 +297,7 @@ export const DefaultsSettings: React.FC = () => {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [smallModelUseDefault, smallModelProviders]);
|
||||
}, [smallModelProviders]);
|
||||
|
||||
const availableVariants = React.useMemo(() => {
|
||||
if (!parsedModel.providerId || !parsedModel.modelId) return [];
|
||||
@@ -396,6 +437,32 @@ export const DefaultsSettings: React.FC = () => {
|
||||
/>
|
||||
</SettingsFieldRow>
|
||||
) : null}
|
||||
|
||||
<SettingsInset className={SETTINGS_OPTION_STACK_CLASS}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<SettingsGroupTitle>
|
||||
{t('settings.openchamber.defaults.walkthroughModel.title')}
|
||||
</SettingsGroupTitle>
|
||||
<SettingsInfoHint>
|
||||
{t('settings.openchamber.defaults.walkthroughModel.description')}
|
||||
</SettingsInfoHint>
|
||||
</div>
|
||||
|
||||
<SettingsFieldRow
|
||||
settingsItem="sessions.walkthrough-model"
|
||||
label={t('settings.openchamber.defaults.walkthroughModel.overrideModel')}
|
||||
>
|
||||
<ModelSelector
|
||||
providerId={parsedWalkthroughModel.providerId}
|
||||
modelId={parsedWalkthroughModel.modelId}
|
||||
onChange={handleWalkthroughModelOverrideChange}
|
||||
allowedProviderIds={smallModelProviders}
|
||||
isModelAllowed={isStructuredOutputCapable}
|
||||
placeholder={t('settings.openchamber.defaults.walkthroughModel.usesSmallModel')}
|
||||
className={SETTINGS_CUSTOM_TRIGGER_CLASS}
|
||||
/>
|
||||
</SettingsFieldRow>
|
||||
</SettingsInset>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
Reference in New Issue
Block a user