Merge main
This commit is contained in:
@@ -25,7 +25,8 @@ import { useDeviceInfo } from '@/lib/device';
|
||||
import { mergeModelMetadataWithLiveModel } from '@/lib/modelMetadata';
|
||||
import { getModelDisplayName as getSharedModelDisplayName } from '@/lib/modelDisplay';
|
||||
import { getEditModeColors } from '@/lib/permissions/editModeColors';
|
||||
import { cn, fuzzyMatch } from '@/lib/utils';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { matchesRankQuery, rankByQuery } from '@/lib/search/fuzzySearch';
|
||||
import { useContextStore } from '@/stores/contextStore';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
@@ -57,6 +58,28 @@ type MobileVariantTarget = { providerId: string; modelId: string };
|
||||
const buildModelRefKey = (providerID: string, modelID: string) => `${providerID}:${modelID}`;
|
||||
const MAX_INLINE_MOBILE_VARIANT_OPTIONS = 6;
|
||||
|
||||
const AgentDescriptionTooltip: React.FC<{
|
||||
description?: string;
|
||||
children: React.ReactElement;
|
||||
}> = ({ description, children }) => {
|
||||
if (!description) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip delayDuration={450}>
|
||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
sideOffset={8}
|
||||
className="max-w-xs text-left transition-none data-[starting-style]:opacity-100 data-[starting-style]:scale-100 data-[ending-style]:opacity-100 data-[ending-style]:scale-100"
|
||||
>
|
||||
<span className="typography-meta text-muted-foreground">{description}</span>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const asPermissionRuleset = (value: unknown): PermissionRule[] | null => {
|
||||
if (!Array.isArray(value)) {
|
||||
return null;
|
||||
@@ -301,7 +324,9 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
const providers = useConfigStore((state) => state.providers);
|
||||
const currentProviderId = useConfigStore((state) => state.currentProviderId);
|
||||
const currentModelId = useConfigStore((state) => state.currentModelId);
|
||||
const currentVariant = useConfigStore((state) => state.currentVariant);
|
||||
const effectiveCurrentVariant = useConfigStore((state) => state.currentVariant);
|
||||
const currentVariantSelection = useConfigStore((state) => state.currentVariantSelection);
|
||||
const currentVariant = currentVariantSelection.override ?? undefined;
|
||||
const currentAgentName = useConfigStore((state) => state.currentAgentName);
|
||||
const settingsDefaultVariant = useConfigStore((state) => state.settingsDefaultVariant);
|
||||
const settingsDefaultAgent = useConfigStore((state) => state.settingsDefaultAgent);
|
||||
@@ -309,6 +334,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
const setSelectedProvider = useConfigStore((state) => state.setSelectedProvider);
|
||||
const setModel = useConfigStore((state) => state.setModel);
|
||||
const setCurrentVariant = useConfigStore((state) => state.setCurrentVariant);
|
||||
const setCurrentVariantOverride = useConfigStore((state) => state.setCurrentVariantOverride);
|
||||
const getCurrentModelVariants = useConfigStore((state) => state.getCurrentModelVariants);
|
||||
const setAgent = useConfigStore((state) => state.setAgent);
|
||||
const getCurrentProvider = useConfigStore((state) => state.getCurrentProvider);
|
||||
@@ -506,13 +532,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
|
||||
const sortedAndFilteredAgents = React.useMemo(() => {
|
||||
const sorted = [...selectableDesktopAgents].sort((a, b) => a.name.localeCompare(b.name));
|
||||
if (!agentSearchQuery.trim()) {
|
||||
return sorted;
|
||||
}
|
||||
return sorted.filter((agent) =>
|
||||
fuzzyMatch(agent.name, agentSearchQuery) ||
|
||||
(agent.description && fuzzyMatch(agent.description, agentSearchQuery))
|
||||
);
|
||||
return rankByQuery(sorted, agentSearchQuery, (agent) => [agent.name, agent.description]);
|
||||
}, [selectableDesktopAgents, agentSearchQuery]);
|
||||
|
||||
const defaultAgentName = React.useMemo(() => {
|
||||
@@ -558,38 +578,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
return result;
|
||||
}, [providers, hiddenModels]);
|
||||
|
||||
const normalizeModelSearchValue = React.useCallback((value: string) => {
|
||||
const lower = value.toLowerCase().trim();
|
||||
const compact = lower.replace(/[^a-z0-9]/g, '');
|
||||
const tokens = lower.split(/[^a-z0-9]+/).filter(Boolean);
|
||||
return { lower, compact, tokens };
|
||||
}, []);
|
||||
|
||||
const matchesModelSearch = React.useCallback((candidate: string, query: string) => {
|
||||
const normalizedQuery = normalizeModelSearchValue(query);
|
||||
if (!normalizedQuery.lower) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const normalizedCandidate = normalizeModelSearchValue(candidate);
|
||||
if (normalizedCandidate.lower.includes(normalizedQuery.lower)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedQuery.compact.length >= 2 && normalizedCandidate.compact.includes(normalizedQuery.compact)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (normalizedQuery.tokens.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return normalizedQuery.tokens.every((queryToken) =>
|
||||
normalizedCandidate.tokens.some((candidateToken) =>
|
||||
candidateToken.startsWith(queryToken) || candidateToken.includes(queryToken)
|
||||
)
|
||||
);
|
||||
}, [normalizeModelSearchValue]);
|
||||
const matchesModelSearch = React.useCallback(
|
||||
(candidate: string, query: string) => matchesRankQuery([candidate], query),
|
||||
[],
|
||||
);
|
||||
|
||||
const currentModelForMetadata = currentModelId
|
||||
? models.find((model: ProviderModel) => model.id === currentModelId)
|
||||
@@ -704,6 +696,30 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
return variants ? Object.keys(variants) : [];
|
||||
}, [providers]);
|
||||
|
||||
const resolveInheritedVariantForModel = React.useCallback((providerId: string, modelId: string, agentName?: string | null) => {
|
||||
const variantOptions = getModelVariantOptions(providerId, modelId);
|
||||
if (variantOptions.length === 0) return undefined;
|
||||
|
||||
let currentInherited: string | undefined;
|
||||
if (currentProviderId === providerId && currentModelId === modelId) {
|
||||
currentInherited = currentVariantSelection.inherited
|
||||
?? (currentVariantSelection.override === null || currentVariantSelection.override === undefined
|
||||
? effectiveCurrentVariant
|
||||
: undefined);
|
||||
}
|
||||
|
||||
const effectiveAgentName = agentName ?? uiAgentName ?? currentAgentName;
|
||||
const agent = effectiveAgentName ? agents.find((candidate) => candidate.name === effectiveAgentName) : undefined;
|
||||
const agentVariant = (
|
||||
agent?.model?.providerID === providerId
|
||||
&& agent.model.modelID === modelId
|
||||
) ? agent.variant : undefined;
|
||||
const candidates = currentSessionId
|
||||
? [agentVariant, settingsDefaultVariant, currentInherited]
|
||||
: [currentInherited, agentVariant, settingsDefaultVariant];
|
||||
return candidates.find((candidate) => candidate !== undefined && variantOptions.includes(candidate));
|
||||
}, [agents, currentAgentName, currentModelId, currentProviderId, currentSessionId, currentVariantSelection, effectiveCurrentVariant, getModelVariantOptions, settingsDefaultVariant, uiAgentName]);
|
||||
|
||||
const resolveModelVariantSelection = React.useCallback((providerId: string, modelId: string) => {
|
||||
const variantOptions = getModelVariantOptions(providerId, modelId);
|
||||
if (variantOptions.length === 0) {
|
||||
@@ -722,10 +738,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
return currentVariant;
|
||||
}
|
||||
|
||||
if (!currentSessionId && settingsDefaultVariant && variantOptions.includes(settingsDefaultVariant)) {
|
||||
return settingsDefaultVariant;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [
|
||||
currentAgentName,
|
||||
@@ -735,7 +747,6 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
currentVariant,
|
||||
getAgentModelVariantForSession,
|
||||
getModelVariantOptions,
|
||||
settingsDefaultVariant,
|
||||
uiAgentName,
|
||||
]);
|
||||
|
||||
@@ -759,7 +770,10 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
}
|
||||
|
||||
manualVariantSelectionRef.current = true;
|
||||
setCurrentVariant(variant);
|
||||
setCurrentVariantOverride(
|
||||
variant ?? null,
|
||||
resolveInheritedVariantForModel(providerId, modelId, agentNameOverride),
|
||||
);
|
||||
addRecentEffort(providerId, modelId, variant);
|
||||
|
||||
const effectiveAgentName = agentNameOverride ?? resolveLiveAgentName();
|
||||
@@ -770,9 +784,11 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
addRecentEffort,
|
||||
currentSessionId,
|
||||
getModelVariantOptions,
|
||||
resolveInheritedVariantForModel,
|
||||
resolveLiveAgentName,
|
||||
saveAgentModelVariantForSession,
|
||||
setCurrentVariant,
|
||||
setCurrentVariantOverride,
|
||||
]);
|
||||
|
||||
const applyModelSelectionWithVariant = React.useCallback((providerId: string, modelId: string, variant: string | undefined, agentNameOverride?: string | null) => {
|
||||
@@ -893,25 +909,29 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
? useSelectionStore.getState().getSessionAgentSelection(currentSessionId)
|
||||
: null;
|
||||
if (savedAgentName) {
|
||||
if (currentAgentName !== savedAgentName) {
|
||||
setAgent(savedAgentName);
|
||||
}
|
||||
|
||||
const savedModel = getAgentModelForSession(currentSessionId, savedAgentName);
|
||||
if (savedModel) {
|
||||
const result = tryApplyModelSelection(savedModel.providerId, savedModel.modelId, savedAgentName);
|
||||
if (result === 'applied') {
|
||||
if (currentAgentName !== savedAgentName) {
|
||||
setAgent(savedAgentName);
|
||||
}
|
||||
return 'resolved';
|
||||
}
|
||||
if (result === 'provider-missing') {
|
||||
return 'waiting';
|
||||
}
|
||||
} else if (currentAgentName !== savedAgentName) {
|
||||
setAgent(savedAgentName);
|
||||
}
|
||||
}
|
||||
|
||||
if (savedSessionModel) {
|
||||
const result = tryApplyModelSelection(savedSessionModel.providerId, savedSessionModel.modelId, savedAgentName || currentAgentName || undefined);
|
||||
if (result === 'applied') {
|
||||
if (savedAgentName && currentAgentName !== savedAgentName) {
|
||||
setAgent(savedAgentName);
|
||||
}
|
||||
return 'resolved';
|
||||
}
|
||||
if (result === 'provider-missing') {
|
||||
@@ -925,16 +945,15 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentAgentName !== agent.name) {
|
||||
setAgent(agent.name);
|
||||
}
|
||||
|
||||
const existingSelection = useSelectionStore.getState().getSessionAgentSelection(currentSessionId) || stickySessionAgentRef.current;
|
||||
if (!existingSelection) {
|
||||
saveSessionAgentSelection(currentSessionId, agent.name);
|
||||
}
|
||||
const result = tryApplyModelSelection(selection.providerId, selection.modelId, agent.name);
|
||||
if (result === 'applied') {
|
||||
if (currentAgentName !== agent.name) {
|
||||
setAgent(agent.name);
|
||||
}
|
||||
const existingSelection = useSelectionStore.getState().getSessionAgentSelection(currentSessionId) || stickySessionAgentRef.current;
|
||||
if (!existingSelection) {
|
||||
saveSessionAgentSelection(currentSessionId, agent.name);
|
||||
}
|
||||
return 'resolved';
|
||||
}
|
||||
if (result === 'provider-missing') {
|
||||
@@ -1129,18 +1148,21 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
}
|
||||
|
||||
if (currentVariant && !availableVariants.includes(currentVariant)) {
|
||||
setCurrentVariant(undefined);
|
||||
setCurrentVariantOverride(
|
||||
null,
|
||||
resolveInheritedVariantForModel(currentProviderId, currentModelId),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Draft state (no session yet): seed from settings default, but don't override
|
||||
// user selection while drafting.
|
||||
if (!currentSessionId) {
|
||||
if (!currentVariant && !manualVariantSelectionRef.current) {
|
||||
if (currentVariantSelection.override === undefined && !manualVariantSelectionRef.current) {
|
||||
const desired = settingsDefaultVariant && availableVariants.includes(settingsDefaultVariant)
|
||||
? settingsDefaultVariant
|
||||
: undefined;
|
||||
setCurrentVariant(desired);
|
||||
setCurrentVariantOverride(desired ?? null, desired);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1152,13 +1174,14 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
currentModelId,
|
||||
);
|
||||
|
||||
const resolvedSaved = savedVariant && availableVariants.includes(savedVariant)
|
||||
? savedVariant
|
||||
: settingsDefaultVariant && availableVariants.includes(settingsDefaultVariant)
|
||||
? settingsDefaultVariant
|
||||
: undefined;
|
||||
|
||||
setCurrentVariant(resolvedSaved);
|
||||
const inheritedVariant = resolveInheritedVariantForModel(currentProviderId, currentModelId);
|
||||
if (savedVariant && availableVariants.includes(savedVariant)) {
|
||||
setCurrentVariantOverride(savedVariant, inheritedVariant);
|
||||
} else if (currentVariantSelection.override === null) {
|
||||
setCurrentVariantOverride(null, inheritedVariant);
|
||||
} else {
|
||||
setCurrentVariant(inheritedVariant);
|
||||
}
|
||||
manualVariantSelectionRef.current = false;
|
||||
}, [
|
||||
availableVariants,
|
||||
@@ -1168,8 +1191,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
currentProviderId,
|
||||
currentModelId,
|
||||
currentVariant,
|
||||
currentVariantSelection.override,
|
||||
effectiveCurrentVariant,
|
||||
getAgentModelVariantForSession,
|
||||
resolveInheritedVariantForModel,
|
||||
setCurrentVariant,
|
||||
setCurrentVariantOverride,
|
||||
settingsDefaultVariant,
|
||||
]);
|
||||
|
||||
@@ -2321,9 +2348,12 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[min(380px,calc(100vw-2rem))] p-0 flex flex-col"
|
||||
side="top"
|
||||
className="w-[min(380px,calc(100vw-2rem))] p-0 flex flex-col overflow-hidden"
|
||||
align="end"
|
||||
alignOffset={-40}
|
||||
constrainToMain
|
||||
collisionAvoidance={{ side: 'none', align: 'shift' }}
|
||||
onKeyDownCapture={handleModelShortcutKeyDownCapture}
|
||||
>
|
||||
<div className="p-1 border-b border-border/40">
|
||||
@@ -2380,6 +2410,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
maxHeightClassName="max-h-[min(400px,calc(var(--available-height)-4rem))] flex-1"
|
||||
tooltipsEnabled={agentMenuOpen}
|
||||
onEscape={() => setAgentMenuOpen(false)}
|
||||
/>
|
||||
@@ -2623,7 +2654,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<DropdownMenuContent align="end" alignOffset={-40} className="w-[min(180px,calc(100vw-2rem))]">
|
||||
<DropdownMenuContent side="top" align="end" alignOffset={-40} className="w-[min(180px,calc(100vw-2rem))]">
|
||||
<DropdownMenuLabel className="typography-ui-header font-semibold text-foreground">{t('chat.modelControls.thinking')}</DropdownMenuLabel>
|
||||
<DropdownMenuItem className="typography-meta" onSelect={() => handleVariantSelect(undefined)}>
|
||||
<div className="flex items-center justify-between gap-2 w-full min-w-0">
|
||||
@@ -2713,7 +2744,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<DropdownMenuContent align="end" alignOffset={-40} className="w-[min(280px,calc(100vw-2rem))] p-0 flex flex-col">
|
||||
<DropdownMenuContent side="top" align="end" alignOffset={-40} constrainToMain collisionAvoidance={{ side: 'none', align: 'shift' }} className="w-[min(280px,calc(100vw-2rem))] p-0 flex flex-col overflow-hidden">
|
||||
<div className="p-2 border-b border-border/40">
|
||||
<div className="relative">
|
||||
<Icon name="search" className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground" />
|
||||
@@ -2729,7 +2760,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ScrollableOverlay outerClassName="max-h-[min(400px,calc(100dvh-12rem))] flex-1">
|
||||
<ScrollableOverlay outerClassName="max-h-[min(400px,calc(var(--available-height)-4rem))] flex-1">
|
||||
<div className="p-1">
|
||||
{!agentSearchQuery.trim() && defaultAgentName && (
|
||||
<>
|
||||
@@ -2751,12 +2782,11 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
</div>
|
||||
) : (
|
||||
sortedAndFilteredAgents.map((agent) => (
|
||||
<DropdownMenuItem
|
||||
key={agent.name}
|
||||
className="typography-meta"
|
||||
onSelect={() => handleAgentChange(agent.name)}
|
||||
>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<AgentDescriptionTooltip key={agent.name} description={agent.description}>
|
||||
<DropdownMenuItem
|
||||
className="typography-meta"
|
||||
onSelect={() => handleAgentChange(agent.name)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className={cn(
|
||||
'h-1 w-1 rounded-full agent-dot',
|
||||
@@ -2764,13 +2794,8 @@ export const ModelControls: React.FC<ModelControlsProps> = ({
|
||||
)} />
|
||||
<span className="font-medium">{capitalizeAgentName(agent.name)}</span>
|
||||
</div>
|
||||
{agent.description && (
|
||||
<span className="typography-meta text-muted-foreground max-w-[200px] ml-2.5 break-words">
|
||||
{agent.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuItem>
|
||||
</AgentDescriptionTooltip>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user