@@ -15,6 +15,7 @@ import { createPortal } from 'react-dom'
1515import { ArrowLeft , BookOpen , CircleHelp , Clock , Eye , FileSearch , FileText , Globe , ListChecks , MoreHorizontal , MousePointer2 , Pencil , Plus , RefreshCw , Search , Server , Shuffle , Tag , Trash2 , Wrench , X , type LucideIcon } from 'lucide-react'
1616import { showToast } from '../../stores/toastStore'
1717import { useModelCatalogStore } from '../../stores/modelCatalogStore'
18+ import { useProvidersStore } from '../../stores/providersStore'
1819import { useConversationStore } from '../../stores/conversationStore'
1920import { useUIStore } from '../../stores/uiStore'
2021import { useLocale , useT } from '../../contexts/LocaleContext'
@@ -23,6 +24,10 @@ import { ConversationPanel } from '../layout/ConversationPanel'
2324import { DragHandle } from '../layout/DragHandle'
2425import { InputComposer , type InputComposerSubmitPayload } from '../conversation/InputComposer'
2526import { useComposerModelControls } from '../conversation/useComposerModelControls'
27+ import {
28+ createCatalogDefaultPreference ,
29+ PreferenceModelPicker
30+ } from '../conversation/PreferenceModelPicker'
2631import { MarkdownRenderer } from '../conversation/MarkdownRenderer'
2732import type { ThreadConfigurationWire } from '../../types/thread'
2833import { formatRelativeTime } from '../../utils/relativeTime'
@@ -41,17 +46,22 @@ import { IconButton } from '../ui/IconButton'
4146import { RobotAvatar } from './RobotAvatar'
4247import { AGENT_BUILDER_AVATAR , randomAvatar , resolveProfileAvatar , type AvatarSpec } from './agentAvatar'
4348import { useAgentProfileAvatarStore } from '../../stores/agentProfileAvatarStore'
49+ import {
50+ findProviderPreference ,
51+ mergeProviderPreferences ,
52+ type ModelPreference ,
53+ type ProviderPreferences
54+ } from '../../../shared/modelPreference'
4455import {
4556 AGENT_CONTROL_OPTIONS ,
4657 APPROVAL_OPTIONS ,
47- REASONING_OPTIONS ,
4858 createEmptyDraft ,
4959 parseProfile ,
5060 toMarkdown ,
5161 type AgentControl ,
62+ type AgentProviderPreference ,
5263 type ApprovalPolicy ,
5364 type ProfileDraft ,
54- type ReasoningEffort ,
5565 type SaveTarget
5666} from './agentProfileDraft'
5767import { applyBuilderChange , isBuilderField , type BuilderField , type BuilderToolResult } from './agentBuilderDraftSync'
@@ -91,6 +101,22 @@ interface SkillInfo {
91101 enabled ?: boolean
92102}
93103
104+ function toAgentProviderPreference (
105+ providerId : string ,
106+ preference : ModelPreference
107+ ) : AgentProviderPreference {
108+ return {
109+ providerId,
110+ model : preference . model ,
111+ reasoning : {
112+ enabled : preference . reasoning . enabled ,
113+ effort : preference . reasoning . effort
114+ } ,
115+ speed : preference . speed ,
116+ contextWindow : { mode : preference . contextWindow . mode }
117+ }
118+ }
119+
94120type Filter = 'all' | 'builtIn' | 'user' | 'workspace'
95121
96122type Route =
@@ -133,7 +159,7 @@ const BUILDER_FIELD_LABEL_KEYS: Record<BuilderField, string> = {
133159 'tools.allow' : 'agentBuilder.field.tools' ,
134160 'mcp.servers' : 'agentBuilder.field.mcp' ,
135161 'skills.preload' : 'agentBuilder.field.skills' ,
136- model : 'agentBuilder.field.model' ,
162+ providerPreference : 'agentBuilder.field.model' ,
137163 approval : 'agentBuilder.field.approval' ,
138164 'tools.agentControl' : 'agentBuilder.field.toolControl'
139165}
@@ -1094,6 +1120,7 @@ interface BuilderViewProps {
10941120
10951121function BuilderView ( { route, setRoute, setDraft, toolCatalog, skillCatalog, mcpServers, viewMode, setViewMode, autoSaveState, editingField, agentDriving, onBack, onDelete, onCreate } : BuilderViewProps ) : JSX . Element {
10961122 const locale = useLocale ( )
1123+ const t = useT ( )
10971124 const { draft, avatar } = route
10981125 const nameMissing = ! draft . name . trim ( )
10991126 const preview = viewMode === 'preview'
@@ -1115,18 +1142,104 @@ function BuilderView({ route, setRoute, setDraft, toolCatalog, skillCatalog, mcp
11151142 return ( ) => document . removeEventListener ( 'mousedown' , onDown , true )
11161143 } , [ menuOpen ] )
11171144
1118- const modelOptions = useModelCatalogStore ( ( s ) => s . modelOptions )
1145+ const providers = useProvidersStore ( ( s ) => s . providers )
1146+ const models = useModelCatalogStore ( ( s ) => s . models )
1147+ const modelCatalogStatus = useModelCatalogStore ( ( s ) => s . status )
1148+ const modelCatalogError = useModelCatalogStore ( ( s ) => s . errorMessage )
1149+ const effectiveCatalogProviderId = useModelCatalogStore ( ( s ) => s . providerId )
1150+ const [ workspaceDefaultPreference , setWorkspaceDefaultPreference ] = useState < AgentProviderPreference | null > ( null )
1151+ const [ workspaceProviderPreferences , setWorkspaceProviderPreferences ] = useState < ProviderPreferences > ( { } )
1152+
11191153 useEffect ( ( ) => {
1120- void useModelCatalogStore . getState ( ) . loadIfNeeded ( )
1154+ void useProvidersStore . getState ( ) . reload ( )
1155+ const getCore = window . api . workspaceConfig ?. getCore
1156+ if ( typeof getCore !== 'function' ) return
1157+ void getCore ( ) . then ( ( core ) => {
1158+ const providerId = ( core . workspace . providerId ?? core . userDefaults . providerId ?? '' ) . trim ( )
1159+ const preferences = mergeProviderPreferences (
1160+ core . userDefaults . providerPreferences ,
1161+ core . workspace . providerPreferences
1162+ )
1163+ const preference = findProviderPreference ( preferences , providerId )
1164+ setWorkspaceProviderPreferences ( preferences )
1165+ setWorkspaceDefaultPreference ( preference ? toAgentProviderPreference ( providerId , preference ) : null )
1166+ } ) . catch ( ( ) => undefined )
11211167 } , [ ] )
1122- const modelSelectOptions = useMemo ( ( ) => {
1123- const opts = [ { value : 'inherit' , label : 'Inherit (thread default)' } , ...modelOptions . map ( ( id ) => ( { value : id , label : id } ) ) ]
1124- if ( draft . model && draft . model !== 'inherit' && ! modelOptions . includes ( draft . model ) ) {
1125- opts . push ( { value : draft . model , label : draft . model } )
1168+
1169+ const selectedProviderId = draft . providerPreference ?. providerId ?? null
1170+ useEffect ( ( ) => {
1171+ void useModelCatalogStore . getState ( ) . loadIfNeeded ( false , selectedProviderId )
1172+ } , [ selectedProviderId ] )
1173+
1174+ const selectedModel = useMemo (
1175+ ( ) => models . find ( ( item ) => item . id === draft . providerPreference ?. model ) ?? null ,
1176+ [ draft . providerPreference ?. model , models ]
1177+ )
1178+ const pickerPreference = useMemo < ModelPreference | null > ( ( ) => {
1179+ const preference = draft . providerPreference
1180+ if ( ! preference ) return null
1181+ return {
1182+ model : preference . model ,
1183+ reasoning : {
1184+ enabled : preference . reasoning . enabled ,
1185+ effort : preference . reasoning . effort ,
1186+ output : selectedModel ?. reasoning ?. defaultOutput ?? 'full'
1187+ } ,
1188+ speed : preference . speed ,
1189+ contextWindow : { mode : preference . contextWindow . mode }
1190+ }
1191+ } , [ draft . providerPreference , selectedModel ] )
1192+ const providerSelectOptions = useMemo ( ( ) => {
1193+ const options = providers . map ( ( provider ) => ( { value : provider . id , label : provider . displayName } ) )
1194+ const current = draft . providerPreference ?. providerId
1195+ if ( current && ! providers . some ( ( provider ) => provider . id . toLowerCase ( ) === current . toLowerCase ( ) ) ) {
1196+ options . push ( { value : current , label : current } )
11261197 }
1127- return opts
1128- } , [ modelOptions , draft . model ] )
1198+ return options
1199+ } , [ draft . providerPreference ?. providerId , providers ] )
1200+ const seedProviderPreference = useCallback ( ( ) : AgentProviderPreference | null => {
1201+ if ( workspaceDefaultPreference ) return structuredClone ( workspaceDefaultPreference )
1202+ const providerId = effectiveCatalogProviderId ?? providers [ 0 ] ?. id
1203+ const model = models [ 0 ]
1204+ if ( ! providerId || ! model ) return null
1205+ return toAgentProviderPreference (
1206+ providerId ,
1207+ createCatalogDefaultPreference ( model , model . id )
1208+ )
1209+ } , [ effectiveCatalogProviderId , models , providers , workspaceDefaultPreference ] )
11291210
1211+ const updateProviderPreference = useCallback ( (
1212+ update : ( preference : AgentProviderPreference ) => AgentProviderPreference
1213+ ) : void => {
1214+ setDraft ( ( current ) => current . providerPreference
1215+ ? { ...current , providerPreference : update ( current . providerPreference ) }
1216+ : current )
1217+ } , [ setDraft ] )
1218+
1219+ const selectProvider = useCallback ( ( providerId : string ) : void => {
1220+ updateProviderPreference ( ( preference ) => ( { ...preference , providerId } ) )
1221+ void useModelCatalogStore . getState ( ) . loadIfNeeded ( true , providerId ) . then ( ( ) => {
1222+ const catalog = useModelCatalogStore . getState ( )
1223+ const configured = findProviderPreference ( workspaceProviderPreferences , providerId )
1224+ const seeded = configured
1225+ ?? createCatalogDefaultPreference ( catalog . models [ 0 ] , catalog . models [ 0 ] ?. id ?? '' )
1226+ if ( ! seeded . model ) return
1227+ setDraft ( ( current ) => {
1228+ if ( current . providerPreference ?. providerId !== providerId ) return current
1229+ return {
1230+ ...current ,
1231+ providerPreference : toAgentProviderPreference ( providerId , seeded )
1232+ }
1233+ } )
1234+ } )
1235+ } , [ setDraft , updateProviderPreference , workspaceProviderPreferences ] )
1236+
1237+ const inheritSummary = workspaceDefaultPreference
1238+ ? `${ workspaceDefaultPreference . providerId } · ${ workspaceDefaultPreference . model } `
1239+ : t ( 'agentBuilder.model.inheritDescription' )
1240+ const pinnedProviderUnavailable = draft . providerPreference != null
1241+ && providers . length > 0
1242+ && ! providers . some ( ( provider ) => provider . id . toLowerCase ( ) === draft . providerPreference ! . providerId . toLowerCase ( ) )
11301243 return (
11311244 < div className = "agent-builder" >
11321245 < header className = "agent-builder-edit-head" >
@@ -1269,34 +1382,76 @@ function BuilderView({ route, setRoute, setDraft, toolCatalog, skillCatalog, mcp
12691382 < Section label = "Details" >
12701383 < SettingsGroup >
12711384 < SettingsRow
1272- label = "Model"
1273- controlMinWidth = { 200 }
1385+ label = { t ( 'agentBuilder.model.customSettings' ) }
1386+ description = { draft . providerPreference ? t ( 'agentBuilder.model.customDescription' ) : inheritSummary }
12741387 control = { (
1275- < FieldAnchor field = "model" active = { editingField === 'model' } className = "agent-builder-detail-control" >
1276- < SettingsSelect < string >
1277- value = { draft . model }
1278- onValueChange = { ( v ) => setDraft ( ( d ) => ( { ...d , model : v } ) ) }
1279- disabled = { preview }
1280- style = { { width : '100%' } }
1281- valueProps = { { 'data-agent-builder-marker-target' : '' } }
1282- options = { modelSelectOptions }
1388+ < FieldAnchor field = "providerPreference" active = { editingField === 'providerPreference' } className = "agent-builder-detail-toggle" >
1389+ < PillSwitch
1390+ checked = { draft . providerPreference != null }
1391+ onChange = { ( checked ) => {
1392+ if ( ! checked ) {
1393+ setDraft ( ( current ) => ( { ...current , providerPreference : null } ) )
1394+ return
1395+ }
1396+ const preference = seedProviderPreference ( )
1397+ if ( preference ) setDraft ( ( current ) => ( { ...current , providerPreference : preference } ) )
1398+ } }
1399+ disabled = { preview || ( ! draft . providerPreference && ! seedProviderPreference ( ) ) }
1400+ aria-label = { t ( 'agentBuilder.model.customSettings' ) }
12831401 />
12841402 </ FieldAnchor >
12851403 ) }
12861404 />
1287- < SettingsRow
1288- label = "Reasoning"
1289- controlMinWidth = { 200 }
1290- control = { (
1291- < SettingsSelect < ReasoningEffort >
1292- value = { draft . reasoningEffort }
1293- onValueChange = { ( v ) => setDraft ( ( d ) => ( { ...d , reasoningEffort : v } ) ) }
1294- disabled = { preview }
1295- style = { { width : '100%' } }
1296- options = { REASONING_OPTIONS }
1405+ { draft . providerPreference && (
1406+ < div className = "agent-builder-model-settings" >
1407+ { pinnedProviderUnavailable && (
1408+ < div className = "agent-builder-model-warning" role = "status" >
1409+ { t ( 'agentBuilder.model.providerUnavailable' , { provider : draft . providerPreference . providerId } ) }
1410+ </ div >
1411+ ) }
1412+ < SettingsRow
1413+ label = { t ( 'agentBuilder.model.provider' ) }
1414+ controlMinWidth = { 200 }
1415+ control = { (
1416+ < SettingsSelect < string >
1417+ value = { draft . providerPreference . providerId }
1418+ onValueChange = { selectProvider }
1419+ disabled = { preview }
1420+ style = { { width : '100%' } }
1421+ options = { providerSelectOptions }
1422+ />
1423+ ) }
12971424 />
1298- ) }
1299- />
1425+ { pickerPreference && (
1426+ < SettingsRow
1427+ label = { t ( 'agentBuilder.model.model' ) }
1428+ controlMinWidth = { 200 }
1429+ control = { (
1430+ < PreferenceModelPicker
1431+ preference = { pickerPreference }
1432+ models = { models }
1433+ loading = { modelCatalogStatus === 'loading' }
1434+ disabled = { preview }
1435+ errorMessage = { modelCatalogError }
1436+ manualFallback = { modelCatalogStatus !== 'loading' && models . length === 0 }
1437+ onRetry = { ( ) => {
1438+ void useModelCatalogStore . getState ( ) . loadIfNeeded (
1439+ true ,
1440+ draft . providerPreference ?. providerId ?? null
1441+ )
1442+ } }
1443+ onChange = { ( preference ) => {
1444+ updateProviderPreference ( ( current ) =>
1445+ toAgentProviderPreference ( current . providerId , preference ) )
1446+ } }
1447+ inputId = "agent-builder-provider-model"
1448+ inputAriaLabel = { t ( 'agentBuilder.model.model' ) }
1449+ />
1450+ ) }
1451+ />
1452+ ) }
1453+ </ div >
1454+ ) }
13001455 < SettingsRow
13011456 label = "Tool self-control"
13021457 description = "Whether the agent can manage its own available tools at runtime."
0 commit comments