refactor: move summarization model settings to notifications

- Remove Utility Model controls from Session Defaults and restore that section’s original scope.
- Add Zen-based Summarization Model selector to Notifications under AI Summarization.
- Keep config resolution aligned with zenModel-only utility behavior in useConfigStore and git API flow.
This commit is contained in:
Bohdan Triapitsyn
2026-02-24 16:19:39 +02:00
parent 100d6d33a1
commit 2f3209bd65
4 changed files with 150 additions and 213 deletions
@@ -12,9 +12,6 @@ import { useUIStore } from '@/stores/useUIStore';
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
import { getModifierLabel, cn } from '@/lib/utils';
const UTILITY_PROVIDER_ID = 'zen';
const UTILITY_PREFERRED_MODEL_ID = 'big-pickle';
const getDisplayModel = (
storedModel: string | undefined
): { providerId: string; modelId: string } => {
@@ -28,45 +25,6 @@ const getDisplayModel = (
return { providerId: '', modelId: '' };
};
const getUtilityDisplayModel = (
storedGitProviderId: string | undefined,
storedGitModelId: string | undefined,
zenModel: string | undefined,
providers: Array<{ id: string; models: Array<{ id: string }> }>
): { providerId: string; modelId: string } => {
if (storedGitProviderId && storedGitModelId) {
const provider = providers.find((p) => p.id === storedGitProviderId);
if (provider?.models.some((m) => m.id === storedGitModelId)) {
return { providerId: storedGitProviderId, modelId: storedGitModelId };
}
}
const utilityProvider = providers.find((p) => p.id === UTILITY_PROVIDER_ID);
if (zenModel && utilityProvider?.models.some((m) => m.id === zenModel)) {
return { providerId: UTILITY_PROVIDER_ID, modelId: zenModel };
}
const preferredUtilityModel = utilityProvider?.models.find((m) => m.id === UTILITY_PREFERRED_MODEL_ID);
if (preferredUtilityModel) {
return { providerId: UTILITY_PROVIDER_ID, modelId: preferredUtilityModel.id };
}
if (utilityProvider?.models.length) {
const randomIndex = Math.floor(Math.random() * utilityProvider.models.length);
const randomModel = utilityProvider.models[randomIndex];
if (randomModel?.id) {
return { providerId: UTILITY_PROVIDER_ID, modelId: randomModel.id };
}
}
const firstProvider = providers[0];
if (firstProvider?.models[0]) {
return { providerId: firstProvider.id, modelId: firstProvider.models[0].id };
}
return { providerId: '', modelId: '' };
};
export const DefaultsSettings: React.FC = () => {
const setProvider = useConfigStore((state) => state.setProvider);
const setModel = useConfigStore((state) => state.setModel);
@@ -77,12 +35,6 @@ export const DefaultsSettings: React.FC = () => {
const setSettingsDefaultAgent = useConfigStore((state) => state.setSettingsDefaultAgent);
const settingsAutoCreateWorktree = useConfigStore((state) => state.settingsAutoCreateWorktree);
const setSettingsAutoCreateWorktree = useConfigStore((state) => state.setSettingsAutoCreateWorktree);
const settingsZenModel = useConfigStore((state) => state.settingsZenModel);
const setSettingsZenModel = useConfigStore((state) => state.setSettingsZenModel);
const settingsGitProviderId = useConfigStore((state) => state.settingsGitProviderId);
const settingsGitModelId = useConfigStore((state) => state.settingsGitModelId);
const setSettingsGitProviderId = useConfigStore((state) => state.setSettingsGitProviderId);
const setSettingsGitModelId = useConfigStore((state) => state.setSettingsGitModelId);
const showDeletionDialog = useUIStore((state) => state.showDeletionDialog);
const setShowDeletionDialog = useUIStore((state) => state.setShowDeletionDialog);
const providers = useConfigStore((state) => state.providers);
@@ -93,9 +45,6 @@ export const DefaultsSettings: React.FC = () => {
const [isLoading, setIsLoading] = React.useState(true);
const parsedModel = React.useMemo(() => getDisplayModel(defaultModel), [defaultModel]);
const utilityDisplayModel = React.useMemo(() => {
return getUtilityDisplayModel(settingsGitProviderId, settingsGitModelId, settingsZenModel, providers);
}, [settingsGitProviderId, settingsGitModelId, settingsZenModel, providers]);
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
React.useEffect(() => {
@@ -105,9 +54,6 @@ export const DefaultsSettings: React.FC = () => {
defaultModel?: string;
defaultVariant?: string;
defaultAgent?: string;
zenModel?: string;
gitProviderId?: string;
gitModelId?: string;
} | null = null;
if (!data) {
@@ -124,18 +70,6 @@ export const DefaultsSettings: React.FC = () => {
? ((settings as Record<string, unknown>).defaultVariant as string)
: undefined,
defaultAgent: typeof settings.defaultAgent === 'string' ? settings.defaultAgent : undefined,
zenModel:
typeof (settings as Record<string, unknown>).zenModel === 'string'
? ((settings as Record<string, unknown>).zenModel as string)
: undefined,
gitProviderId:
typeof (settings as Record<string, unknown>).gitProviderId === 'string'
? ((settings as Record<string, unknown>).gitProviderId as string)
: undefined,
gitModelId:
typeof (settings as Record<string, unknown>).gitModelId === 'string'
? ((settings as Record<string, unknown>).gitModelId as string)
: undefined,
};
}
} catch {
@@ -167,25 +101,10 @@ export const DefaultsSettings: React.FC = () => {
typeof data.defaultAgent === 'string' && data.defaultAgent.trim().length > 0
? data.defaultAgent.trim()
: undefined;
const zen =
typeof data.zenModel === 'string' && data.zenModel.trim().length > 0
? data.zenModel.trim()
: undefined;
const gitProviderId =
typeof data.gitProviderId === 'string' && data.gitProviderId.trim().length > 0
? data.gitProviderId.trim()
: undefined;
const gitModelId =
typeof data.gitModelId === 'string' && data.gitModelId.trim().length > 0
? data.gitModelId.trim()
: undefined;
if (model !== undefined) setDefaultModel(model);
if (variant !== undefined) setDefaultVariant(variant);
if (agent !== undefined) setDefaultAgent(agent);
if (zen !== undefined) setSettingsZenModel(zen);
if (gitProviderId !== undefined) setSettingsGitProviderId(gitProviderId);
if (gitModelId !== undefined) setSettingsGitModelId(gitModelId);
}
} catch (error) {
console.warn('Failed to load defaults settings:', error);
@@ -194,7 +113,7 @@ export const DefaultsSettings: React.FC = () => {
}
};
loadSettings();
}, [setSettingsGitModelId, setSettingsGitProviderId, setSettingsZenModel]);
}, []);
const handleModelChange = React.useCallback(
async (providerId: string, modelId: string) => {
@@ -303,26 +222,6 @@ export const DefaultsSettings: React.FC = () => {
[setSettingsAutoCreateWorktree]
);
const handleUtilityModelChange = React.useCallback(
async (providerId: string, modelId: string) => {
setSettingsGitProviderId(providerId);
setSettingsGitModelId(modelId);
if (providerId === UTILITY_PROVIDER_ID) {
setSettingsZenModel(modelId);
}
try {
await updateDesktopSettings({
gitProviderId: providerId,
gitModelId: modelId,
...(providerId === UTILITY_PROVIDER_ID ? { zenModel: modelId } : {}),
});
} catch (error) {
console.warn('Failed to save utility model setting:', error);
}
},
[setSettingsGitModelId, setSettingsGitProviderId, setSettingsZenModel]
);
if (isLoading) {
return null;
}
@@ -393,29 +292,6 @@ export const DefaultsSettings: React.FC = () => {
</div>
</div>
<div className="flex flex-col gap-2 py-1 sm:flex-row sm:items-center sm:gap-8">
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
<div className="flex items-center gap-2">
<span className="typography-ui-label text-foreground">Utility Model</span>
<Tooltip delayDuration={1000}>
<TooltipTrigger asChild>
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
</TooltipTrigger>
<TooltipContent sideOffset={8} className="max-w-xs">
The model used for lightweight background tasks like commit messages, PR descriptions, and summarization.
</TooltipContent>
</Tooltip>
</div>
</div>
<div className="flex min-w-0 flex-1 items-center gap-2 sm:w-fit sm:flex-initial">
<ModelSelector
providerId={utilityDisplayModel.providerId}
modelId={utilityDisplayModel.modelId}
onChange={handleUtilityModelChange}
/>
</div>
</div>
<div
className="group flex cursor-pointer items-center gap-2 py-1"
role="button"
@@ -1,8 +1,10 @@
import React from 'react';
import { RiRestartLine } from '@remixicon/react';
import { RiInformationLine, RiRestartLine } from '@remixicon/react';
import { useUIStore } from '@/stores/useUIStore';
import { useConfigStore } from '@/stores/useConfigStore';
import { isDesktopShell, isVSCodeRuntime } from '@/lib/desktop';
import { useDeviceInfo } from '@/lib/device';
import { updateDesktopSettings } from '@/lib/persistence';
import { Checkbox } from '@/components/ui/checkbox';
import { toast } from '@/components/ui';
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
@@ -10,6 +12,8 @@ import { GridLoader } from '@/components/ui/grid-loader';
import { Input } from '@/components/ui/input';
import { NumberInput } from '@/components/ui/number-input';
import { ButtonSmall } from '@/components/ui/button-small';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
const DEFAULT_NOTIFICATION_TEMPLATES = {
@@ -19,6 +23,10 @@ const DEFAULT_NOTIFICATION_TEMPLATES = {
subtask: { title: '{agent_name} is ready', message: '{model_name} completed the task' },
} as const;
const UTILITY_PROVIDER_ID = 'zen';
const UTILITY_PREFERRED_MODEL_ID = 'big-pickle';
const UTILITY_NOT_SELECTED_VALUE = '__not_selected__';
const DEFAULT_SUMMARY_THRESHOLD = 200;
const DEFAULT_SUMMARY_LENGTH = 100;
const DEFAULT_MAX_LAST_MESSAGE_LENGTH = 250;
@@ -50,11 +58,105 @@ export const NotificationSettings: React.FC = () => {
const setSummaryLength = useUIStore(state => state.setSummaryLength);
const maxLastMessageLength = useUIStore(state => state.maxLastMessageLength);
const setMaxLastMessageLength = useUIStore(state => state.setMaxLastMessageLength);
const providers = useConfigStore((state) => state.providers);
const settingsZenModel = useConfigStore((state) => state.settingsZenModel);
const setSettingsZenModel = useConfigStore((state) => state.setSettingsZenModel);
const [notificationPermission, setNotificationPermission] = React.useState<NotificationPermission>('default');
const [pushSupported, setPushSupported] = React.useState(false);
const [pushSubscribed, setPushSubscribed] = React.useState(false);
const [pushBusy, setPushBusy] = React.useState(false);
const [fetchedZenModels, setFetchedZenModels] = React.useState<Array<{ id: string; name: string }>>([]);
const providerZenModels = React.useMemo(() => {
const zenProvider = providers.find((provider) => provider.id === UTILITY_PROVIDER_ID);
const models = Array.isArray(zenProvider?.models) ? zenProvider.models : [];
return models
.map((model: Record<string, unknown>) => {
const id = typeof model.id === 'string' ? model.id.trim() : '';
if (!id) {
return null;
}
const name = typeof model.name === 'string' && model.name.trim().length > 0 ? model.name.trim() : id;
return { id, name };
})
.filter((model): model is { id: string; name: string } => model !== null);
}, [providers]);
React.useEffect(() => {
if (providerZenModels.length > 0) {
setFetchedZenModels([]);
return;
}
const controller = new AbortController();
void fetch('/api/zen/models', {
method: 'GET',
headers: { Accept: 'application/json' },
signal: controller.signal,
})
.then(async (response) => {
if (!response.ok) {
return [] as Array<{ id: string; name: string }>;
}
const payload = await response.json().catch(() => ({}));
const models = Array.isArray(payload?.models) ? payload.models : [];
return models
.map((entry: unknown) => {
const id = typeof (entry as { id?: unknown })?.id === 'string'
? (entry as { id: string }).id.trim()
: '';
if (!id) {
return null;
}
return { id, name: id };
})
.filter((entry: { id: string; name: string } | null): entry is { id: string; name: string } => entry !== null);
})
.then((models) => {
setFetchedZenModels(models);
})
.catch((error) => {
if (error?.name !== 'AbortError') {
console.warn('Failed to load zen utility models:', error);
}
});
return () => {
controller.abort();
};
}, [providerZenModels]);
const utilityModelOptions = React.useMemo(() => {
return providerZenModels.length > 0 ? providerZenModels : fetchedZenModels;
}, [fetchedZenModels, providerZenModels]);
const utilitySelectedModelId = React.useMemo(() => {
if (settingsZenModel && utilityModelOptions.some((model) => model.id === settingsZenModel)) {
return settingsZenModel;
}
if (utilityModelOptions.some((model) => model.id === UTILITY_PREFERRED_MODEL_ID)) {
return UTILITY_PREFERRED_MODEL_ID;
}
return utilityModelOptions[0]?.id ?? '';
}, [settingsZenModel, utilityModelOptions]);
const handleUtilityModelChange = React.useCallback(
async (value: string) => {
const modelId = value === UTILITY_NOT_SELECTED_VALUE ? undefined : value;
setSettingsZenModel(modelId);
try {
await updateDesktopSettings({
zenModel: modelId ?? '',
gitProviderId: '',
gitModelId: '',
});
} catch (error) {
console.warn('Failed to save utility model setting:', error);
}
},
[setSettingsZenModel]
);
React.useEffect(() => {
if (!isBrowser) {
@@ -648,6 +750,40 @@ export const NotificationSettings: React.FC = () => {
<span className="typography-ui-label text-foreground">Summarize Last Message</span>
</div>
<div className={cn("flex flex-col gap-2 py-1 sm:flex-row sm:items-center sm:gap-8")}>
<div className="flex min-w-0 flex-col sm:w-56 shrink-0">
<div className="flex items-center gap-2">
<span className="typography-ui-label text-foreground">Summarization Model</span>
<Tooltip delayDuration={1000}>
<TooltipTrigger asChild>
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
</TooltipTrigger>
<TooltipContent sideOffset={8} className="max-w-xs">
Used for notification and voice summaries.
</TooltipContent>
</Tooltip>
</div>
</div>
<div className="flex min-w-0 flex-1 items-center gap-2 sm:w-fit sm:flex-initial">
<Select
value={utilitySelectedModelId || UTILITY_NOT_SELECTED_VALUE}
onValueChange={handleUtilityModelChange}
>
<SelectTrigger className="w-fit min-w-[220px]">
<SelectValue placeholder="Not selected" />
</SelectTrigger>
<SelectContent>
<SelectItem value={UTILITY_NOT_SELECTED_VALUE}>Not selected</SelectItem>
{utilityModelOptions.map((model) => (
<SelectItem key={model.id} value={model.id}>
{model.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{summarizeLastMessage ? (
<>
<div className="flex items-center gap-8 py-1.5 mt-1 border-t border-[var(--surface-subtle)]">