feat(ui): show recent used models in chat footer model picker (#11097)
Signed-off-by: Abhijay Jain <Abhijay007j@gmail.com>
This commit is contained in:
@@ -29,6 +29,7 @@ vi.mock('../../../ConfigContext', () => ({
|
||||
|
||||
vi.mock('../modelInterface', () => ({
|
||||
getProviderMetadata: vi.fn().mockResolvedValue({ display_name: 'Config Provider' }),
|
||||
fetchModelReasoning: vi.fn().mockResolvedValue(null),
|
||||
}));
|
||||
|
||||
vi.mock('../predefinedModelsUtils', () => ({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Sliders, Bot, LoaderCircle, Settings } from 'lucide-react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Sliders, Bot, LoaderCircle, Settings, History } from 'lucide-react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useModelAndProvider } from '../../../ModelAndProviderContext';
|
||||
import { SwitchModelModal } from '../subcomponents/SwitchModelModal';
|
||||
import { View } from '../../../../utils/navigationUtils';
|
||||
@@ -7,15 +7,20 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '../../../ui/dropdown-menu';
|
||||
import { getProviderMetadata } from '../modelInterface';
|
||||
import { getProviderMetadata, fetchModelReasoning } from '../modelInterface';
|
||||
import { getModelDisplayName } from '../predefinedModelsUtils';
|
||||
import { acpReadThinkingEffort } from '../../../../acp/providers';
|
||||
|
||||
import { ModelSettingsPanel } from '../../localInference/ModelSettingsPanel';
|
||||
import { ScrollArea } from '../../../ui/scroll-area';
|
||||
import { defineMessages, useIntl } from '../../../../i18n';
|
||||
import type { Message } from '../../../../types/message';
|
||||
import type { RecentModel } from '../../../../utils/settings';
|
||||
import { addToRecentModels } from '../../../../utils/recentModels';
|
||||
import { trackModelChanged } from '../../../../utils/analytics';
|
||||
|
||||
const i18n = defineMessages({
|
||||
selectModel: {
|
||||
@@ -46,6 +51,10 @@ const i18n = defineMessages({
|
||||
id: 'modelsBottomBar.resolvedModel',
|
||||
defaultMessage: 'Resolved model',
|
||||
},
|
||||
recentModels: {
|
||||
id: 'modelsBottomBar.recentModels',
|
||||
defaultMessage: 'Recent',
|
||||
},
|
||||
});
|
||||
|
||||
interface ModelsBottomBarProps {
|
||||
@@ -71,7 +80,7 @@ export default function ModelsBottomBar({
|
||||
}: ModelsBottomBarProps) {
|
||||
// ChatInput owns the override state and passes effective model/provider as sessionModel/sessionProvider.
|
||||
// Fall back to config defaults when no session-specific model is available.
|
||||
const { currentModel: configModel, currentProvider: configProvider } = useModelAndProvider();
|
||||
const { currentModel: configModel, currentProvider: configProvider, changeModel } = useModelAndProvider();
|
||||
const currentModel = sessionModel ?? configModel;
|
||||
const currentProvider = sessionProvider ?? configProvider;
|
||||
|
||||
@@ -83,6 +92,16 @@ export default function ModelsBottomBar({
|
||||
const [isAddModelModalOpen, setIsAddModelModalOpen] = useState(false);
|
||||
const [isLocalModelSettingsOpen, setIsLocalModelSettingsOpen] = useState(false);
|
||||
const [providerDefaultModel, setProviderDefaultModel] = useState<string | null>(null);
|
||||
const [recentModels, setRecentModels] = useState<RecentModel[]>([]);
|
||||
|
||||
const loadRecentModels = useCallback(async () => {
|
||||
const stored = (await window.electron.getSetting('recentModels')) ?? [];
|
||||
setRecentModels(stored);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadRecentModels();
|
||||
}, [loadRecentModels]);
|
||||
|
||||
// Show a visible loading placeholder while session metadata is still being fetched,
|
||||
// rather than flashing the config default or leaving the footer blank.
|
||||
@@ -139,9 +158,41 @@ export default function ModelsBottomBar({
|
||||
);
|
||||
|
||||
const handleModelSelected = (model: string, provider: string) => {
|
||||
void loadRecentModels();
|
||||
onModelChanged({ model, provider });
|
||||
};
|
||||
|
||||
const handleRecentModelClick = async (recent: RecentModel) => {
|
||||
const previousModel = currentModel;
|
||||
const previousProvider = currentProvider;
|
||||
|
||||
const [reasoning, savedEffort] = await Promise.all([
|
||||
fetchModelReasoning(recent.provider, recent.model),
|
||||
acpReadThinkingEffort().catch(() => null),
|
||||
]);
|
||||
const modelArg = reasoning
|
||||
? {
|
||||
name: recent.model,
|
||||
provider: recent.provider,
|
||||
request_params: { thinking_effort: savedEffort ?? 'off' },
|
||||
}
|
||||
: { name: recent.model, provider: recent.provider };
|
||||
const success = await changeModel(sessionId, modelArg);
|
||||
if (success) {
|
||||
trackModelChanged(recent.provider, recent.model);
|
||||
if (previousModel && previousProvider) {
|
||||
const updated = addToRecentModels(recentModels, previousProvider, previousModel);
|
||||
await window.electron.setSetting('recentModels', updated);
|
||||
setRecentModels(updated);
|
||||
}
|
||||
onModelChanged({ model: recent.model, provider: recent.provider });
|
||||
}
|
||||
};
|
||||
|
||||
const filteredRecentModels = recentModels.filter(
|
||||
(r) => !(r.model === currentModel && r.provider === currentProvider)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex items-center" ref={dropdownRef}>
|
||||
<DropdownMenu>
|
||||
@@ -179,6 +230,23 @@ export default function ModelsBottomBar({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{filteredRecentModels.length > 0 && (
|
||||
<>
|
||||
<h6 className="text-xs text-text-primary mt-2 ml-2">
|
||||
{intl.formatMessage(i18n.recentModels)}
|
||||
</h6>
|
||||
{filteredRecentModels.map((recent) => (
|
||||
<DropdownMenuItem
|
||||
key={`${recent.provider}/${recent.model}`}
|
||||
onClick={() => void handleRecentModelClick(recent)}
|
||||
>
|
||||
<History className="mr-2 h-3.5 w-3.5 flex-shrink-0 text-text-secondary" />
|
||||
<span className="truncate">{getModelDisplayName(recent.model)} — {recent.provider}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem onClick={() => setIsAddModelModalOpen(true)}>
|
||||
<span>{intl.formatMessage(i18n.changeModel)}</span>
|
||||
<Sliders className="ml-auto h-4 w-4 rotate-90" />
|
||||
|
||||
@@ -29,6 +29,7 @@ import Model, {
|
||||
import { getPredefinedModelsFromEnv, shouldShowPredefinedModels } from '../predefinedModelsUtils';
|
||||
import type { ProviderDetails, ProviderType, ThinkingEffort } from '../../../../types/providers';
|
||||
import { trackModelChanged } from '../../../../utils/analytics';
|
||||
import { addToRecentModels } from '../../../../utils/recentModels';
|
||||
|
||||
const i18n = defineMessages({
|
||||
thinkingEffortOff: {
|
||||
@@ -425,8 +426,15 @@ export const SwitchModelModal = ({
|
||||
|
||||
const success = await changeModel(sessionId, modelObj);
|
||||
if (success) {
|
||||
onModelSelected?.(modelObj.name, modelObj.provider || '');
|
||||
trackModelChanged(modelObj.provider || '', modelObj.name);
|
||||
if (currentModel && currentProvider) {
|
||||
const current = (await window.electron.getSetting('recentModels')) ?? [];
|
||||
await window.electron.setSetting(
|
||||
'recentModels',
|
||||
addToRecentModels(current, currentProvider, currentModel)
|
||||
);
|
||||
}
|
||||
onModelSelected?.(modelObj.name, modelObj.provider || '');
|
||||
}
|
||||
|
||||
onClose();
|
||||
|
||||
Reference in New Issue
Block a user