@@ -38,6 +38,7 @@ import {
3838 SUBAGENT_PRESET_DESCRIPTION_MAX_CHARS ,
3939 SUBAGENT_PRESET_ID_MAX_CHARS ,
4040 SUBAGENT_PRESET_NAME_MAX_CHARS ,
41+ type AdHocSubagentPolicy ,
4142 type SubagentPreset ,
4243 type SubagentProfile ,
4344} from '@maka/core/subagent-settings' ;
@@ -94,6 +95,10 @@ type SubagentEditorDraft = Omit<SubagentPreset, 'thinkingLevel'> & {
9495 thinkingLevel : ThinkingLevel | '' ;
9596} ;
9697
98+ type AdHocSubagentPolicyDraft = Omit < AdHocSubagentPolicy , 'thinkingLevel' > & {
99+ thinkingLevel : ThinkingLevel | '' ;
100+ } ;
101+
97102export function SubagentSettingsPage ( props : {
98103 settings : AppSettings ;
99104 connections : readonly ( LlmConnection & HostResolvedConnectionCatalog ) [ ] ;
@@ -158,7 +163,14 @@ export function SubagentSettingsPage(props: {
158163 ) : Promise < boolean > {
159164 setSaving ( true ) ;
160165 try {
161- const result = await props . onUpdate ( { subagents : { presets : nextPresets } } ) ;
166+ const result = await props . onUpdate ( {
167+ subagents : {
168+ presets : nextPresets ,
169+ ...( props . settings . subagents . adHoc
170+ ? { adHoc : props . settings . subagents . adHoc }
171+ : { } ) ,
172+ } ,
173+ } ) ;
162174 if (
163175 expectPresent !== undefined &&
164176 ! result . settings . subagents . presets . some ( ( candidate ) => candidate . id === expectPresent )
@@ -236,6 +248,22 @@ export function SubagentSettingsPage(props: {
236248
237249 return (
238250 < SettingsPage >
251+ < AdHocSubagentPolicySection
252+ key = { JSON . stringify ( props . settings . subagents . adHoc ?? null ) }
253+ policy = { props . settings . subagents . adHoc }
254+ connections = { props . connections }
255+ isSaving = { saving }
256+ onSave = { async ( adHoc ) => {
257+ setSaving ( true ) ;
258+ try {
259+ await props . onUpdate ( { subagents : { presets, adHoc } } ) ;
260+ } catch ( error ) {
261+ reportHostError ( copy . adHoc . saveFailed , settingsActionErrorMessage ( error , locale ) ) ;
262+ } finally {
263+ setSaving ( false ) ;
264+ }
265+ } }
266+ />
239267 < SettingsSection
240268 title = { copy . section . title }
241269 /* The 「/ 64」 was a system ceiling nobody can raise or act on;
@@ -331,6 +359,176 @@ export function SubagentSettingsPage(props: {
331359 ) ;
332360}
333361
362+ function AdHocSubagentPolicySection ( props : {
363+ policy : AdHocSubagentPolicy | undefined ;
364+ connections : readonly ( LlmConnection & HostResolvedConnectionCatalog ) [ ] ;
365+ isSaving : boolean ;
366+ onSave ( policy : AdHocSubagentPolicy ) : Promise < void > ;
367+ } ) {
368+ const locale = useUiLocale ( ) ;
369+ const copy = getSubagentSettingsCopy ( locale ) ;
370+ const usableConnections = useMemo (
371+ ( ) => props . connections . filter ( isSelectableSubagentConnection ) ,
372+ [ props . connections ] ,
373+ ) ;
374+ const policy = props . policy ;
375+ const initialConnection = policy
376+ ? props . connections . find ( ( connection ) => connection . slug === policy . connectionSlug )
377+ : usableConnections [ 0 ] ;
378+ const initialModels = initialConnection ? offerableCatalogEntries ( initialConnection ) : [ ] ;
379+ const [ draft , setDraft ] = useState < AdHocSubagentPolicyDraft > ( ( ) => ( {
380+ enabled : props . policy ?. enabled ?? false ,
381+ maxProfile : props . policy ?. maxProfile ?? 'local_read' ,
382+ connectionSlug : props . policy ?. connectionSlug ?? usableConnections [ 0 ] ?. slug ?? '' ,
383+ model : props . policy ?. model ?? initialModels [ 0 ] ?. id ?? '' ,
384+ thinkingLevel : props . policy ?. thinkingLevel ?? '' ,
385+ } ) ) ;
386+ const selectedConnection = props . connections . find (
387+ ( connection ) => connection . slug === draft . connectionSlug ,
388+ ) ;
389+ const offerableModels = selectedConnection ? offerableCatalogEntries ( selectedConnection ) : [ ] ;
390+ const thinkingLevels =
391+ selectedConnection ?. catalogEntries . find ( ( entry ) => entry . id === draft . model ) ?. thinkingLevels ??
392+ [ ] ;
393+ const validRoute = Boolean (
394+ selectedConnection &&
395+ isSelectableSubagentConnection ( selectedConnection ) &&
396+ offerableModels . some ( ( entry ) => entry . id === draft . model ) ,
397+ ) ;
398+ const canSave = validRoute || ( props . policy !== undefined && ! draft . enabled ) ;
399+
400+ function selectConnection ( connectionSlug : string ) : void {
401+ const connection = usableConnections . find ( ( candidate ) => candidate . slug === connectionSlug ) ;
402+ const models = connection ? offerableCatalogEntries ( connection ) : [ ] ;
403+ setDraft ( ( current ) => ( {
404+ ...current ,
405+ connectionSlug,
406+ model : models [ 0 ] ?. id ?? '' ,
407+ thinkingLevel : '' ,
408+ } ) ) ;
409+ }
410+
411+ function policyFromDraft ( next : AdHocSubagentPolicyDraft ) : AdHocSubagentPolicy {
412+ return {
413+ enabled : next . enabled ,
414+ maxProfile : next . maxProfile ,
415+ connectionSlug : next . connectionSlug ,
416+ model : next . model ,
417+ ...( next . thinkingLevel ? { thinkingLevel : next . thinkingLevel } : { } ) ,
418+ } ;
419+ }
420+
421+ return (
422+ < SettingsSection title = { copy . adHoc . title } description = { copy . adHoc . description } >
423+ < SettingsRow
424+ label = { copy . adHoc . enabled }
425+ description = { copy . adHoc . enabledDescription }
426+ align = "start"
427+ end = { (
428+ < Switch
429+ label = { copy . adHoc . enabled }
430+ isLabelHidden
431+ value = { draft . enabled }
432+ isDisabled = { props . isSaving || ( ! validRoute && ! draft . enabled ) }
433+ onChange = { ( enabled ) => setDraft ( ( current ) => ( { ...current , enabled } ) ) }
434+ />
435+ ) }
436+ />
437+ < SettingsRow
438+ label = { copy . adHoc . profile }
439+ description = { copy . adHoc . profileDescription }
440+ end = { (
441+ < Selector
442+ label = { copy . adHoc . profile }
443+ isLabelHidden
444+ value = { draft . maxProfile }
445+ options = { ( Object . keys ( copy . profiles ) as SubagentProfile [ ] ) . map ( ( profile ) => ( {
446+ value : profile ,
447+ label : copy . profiles [ profile ] . label ,
448+ } ) ) }
449+ width = "100%"
450+ isDisabled = { props . isSaving }
451+ onChange = { ( maxProfile ) => setDraft ( ( current ) => ( {
452+ ...current ,
453+ maxProfile : maxProfile as SubagentProfile ,
454+ } ) ) }
455+ />
456+ ) }
457+ />
458+ < SettingsRow
459+ label = { copy . adHoc . connection }
460+ end = { (
461+ < Selector
462+ label = { copy . adHoc . connection }
463+ isLabelHidden
464+ value = { draft . connectionSlug }
465+ options = { usableConnections . map ( ( connection ) => ( {
466+ value : connection . slug ,
467+ label : connection . name ,
468+ } ) ) }
469+ width = "100%"
470+ isDisabled = { props . isSaving || usableConnections . length === 0 }
471+ disabledMessage = { usableConnections . length === 0 ? copy . adHoc . noConnection : undefined }
472+ onChange = { selectConnection }
473+ />
474+ ) }
475+ />
476+ < SettingsRow
477+ label = { copy . adHoc . model }
478+ end = { (
479+ < Selector
480+ label = { copy . adHoc . model }
481+ isLabelHidden
482+ value = { draft . model }
483+ options = { offerableModels . map ( ( entry ) => ( {
484+ value : entry . id ,
485+ label : entry . displayName ?. trim ( ) || entry . id ,
486+ } ) ) }
487+ width = "100%"
488+ isDisabled = { props . isSaving || offerableModels . length === 0 }
489+ disabledMessage = { offerableModels . length === 0 ? copy . adHoc . noModel : undefined }
490+ onChange = { ( model ) => setDraft ( ( current ) => ( {
491+ ...current ,
492+ model,
493+ thinkingLevel : '' ,
494+ } ) ) }
495+ />
496+ ) }
497+ />
498+ { thinkingLevels . length > 0 ? (
499+ < SettingsRow
500+ label = { copy . adHoc . thinking }
501+ end = { (
502+ < Selector
503+ label = { copy . adHoc . thinking }
504+ isLabelHidden
505+ value = { draft . thinkingLevel }
506+ options = { [
507+ { value : '' , label : copy . editor . defaultThinking } ,
508+ ...thinkingLevels . map ( ( level ) => ( { value : level , label : copy . thinking [ level ] } ) ) ,
509+ ] }
510+ width = "100%"
511+ isDisabled = { props . isSaving }
512+ onChange = { ( thinkingLevel ) => setDraft ( ( current ) => ( {
513+ ...current ,
514+ thinkingLevel : thinkingLevel as ThinkingLevel | '' ,
515+ } ) ) }
516+ />
517+ ) }
518+ />
519+ ) : null }
520+ < HStack gap = { 2 } wrap = "wrap" >
521+ < Button
522+ variant = "primary"
523+ label = { copy . adHoc . save }
524+ isDisabled = { props . isSaving || ! canSave }
525+ onClick = { ( ) => void props . onSave ( policyFromDraft ( draft ) ) }
526+ />
527+ </ HStack >
528+ </ SettingsSection >
529+ ) ;
530+ }
531+
334532function SubagentPresetEditor ( props : {
335533 preset : SubagentPreset | null ;
336534 presets : readonly SubagentPreset [ ] ;
0 commit comments