@@ -3,6 +3,7 @@ import { ProjectSecretField } from '@/components/projects/project-secret-field.j
33import { useProjectUpdate } from '@/components/projects/use-project-update.js' ;
44import { EngineSettingsFields } from '@/components/settings/engine-settings-fields.js' ;
55import { ModelField } from '@/components/settings/model-field.js' ;
6+ import { Badge } from '@/components/ui/badge.js' ;
67import {
78 Card ,
89 CardContent ,
@@ -13,13 +14,7 @@ import {
1314} from '@/components/ui/card.js' ;
1415import { Input } from '@/components/ui/input.js' ;
1516import { Label } from '@/components/ui/label.js' ;
16- import {
17- Select ,
18- SelectContent ,
19- SelectItem ,
20- SelectTrigger ,
21- SelectValue ,
22- } from '@/components/ui/select.js' ;
17+ import { Tabs , TabsContent , TabsList , TabsTrigger } from '@/components/ui/tabs.js' ;
2318import {
2419 Tooltip ,
2520 TooltipContent ,
@@ -47,7 +42,7 @@ function capitalize(s: string): string {
4742 return s . charAt ( 0 ) . toUpperCase ( ) + s . slice ( 1 ) ;
4843}
4944
50- // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: multiple query dependencies and credential sections for engine-specific settings rendering
45+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: multiple query dependencies and per-engine tab rendering for credentials and settings
5146export function ProjectHarnessForm ( { project } : { project : Project } ) {
5247 const updateMutation = useProjectUpdate ( project . id ) ;
5348 const enginesQuery = useQuery ( trpc . agentConfigs . engines . queryOptions ( ) ) ;
@@ -70,48 +65,39 @@ export function ProjectHarnessForm({ project }: { project: Project }) {
7065 project . engineSettings ?? { } ,
7166 ) ;
7267
73- const effectiveEngineId = agentEngine || '' ;
74- const effectiveEngine = enginesQuery . data ?. find ( ( engine ) => engine . id === effectiveEngineId ) ;
68+ // Derived values
69+ const engines = enginesQuery . data ?? [ ] ;
70+ const credentials = credentialsQuery . data ?? [ ] ;
71+ const agentEnginesInUse = enginesInUseQuery . data ?? [ ] ;
72+
73+ // System default engine (e.g. 'claude-code') shown when no project-level engine is set
74+ const systemDefaultEngineId = defaults ?. agentEngine ?? 'claude-code' ;
75+ // The effective project-level engine: either explicitly set or the system default
76+ const effectiveEngineId = agentEngine || systemDefaultEngineId ;
77+
78+ // Default tab to show: project's selected engine, or system default
79+ const defaultTab = effectiveEngineId ;
7580
76- // Resolved engine defaults for the EngineSettingsFields component
77- const engineDefaults =
78- defaults && effectiveEngineId
79- ? ( defaults . engineSettings as Record < string , Record < string , unknown > > ) [ effectiveEngineId ]
81+ // Resolved engine defaults for EngineSettingsFields
82+ function getEngineDefaults ( engineId : string ) : Record < string , unknown > | undefined {
83+ return defaults
84+ ? ( defaults . engineSettings as Record < string , Record < string , unknown > > ) [ engineId ]
8085 : undefined ;
86+ }
8187
8288 function handleSubmit ( e : React . FormEvent ) {
8389 e . preventDefault ( ) ;
8490 const activeEngine = agentEngine || null ;
85- const activeEngineSettings =
86- activeEngine && engineSettings [ activeEngine ]
87- ? { [ activeEngine ] : engineSettings [ activeEngine ] }
88- : null ;
91+ // Save all engine settings, not just the active engine
92+ const allEngineSettings = Object . keys ( engineSettings ) . length > 0 ? engineSettings : null ;
8993 updateMutation . mutate ( {
9094 model : model || null ,
9195 maxIterations : maxIterations ? Number . parseInt ( maxIterations , 10 ) : null ,
9296 agentEngine : activeEngine ,
93- engineSettings : activeEngineSettings ,
97+ engineSettings : allEngineSettings ,
9498 } ) ;
9599 }
96100
97- const credentials = credentialsQuery . data ?? [ ] ;
98-
99- // Collect all engine IDs that need credentials:
100- // 1. The project-level selected engine (effectiveEngineId)
101- // 2. Any per-agent engine overrides from agent configs
102- const agentEnginesInUse = enginesInUseQuery . data ?? [ ] ;
103- const allEnginesInUse = effectiveEngineId
104- ? Array . from ( new Set ( [ effectiveEngineId , ...agentEnginesInUse ] ) )
105- : agentEnginesInUse ;
106-
107- // Show engine secrets for all engines in use (grouped by engine when multiple)
108- const visibleSecrets = ENGINE_SECRETS . filter (
109- ( s ) => ! s . engines || s . engines . some ( ( e ) => allEnginesInUse . includes ( e ) ) ,
110- ) ;
111-
112- // Default engine label for the select placeholder
113- const defaultEngineLabel = defaults ? `Default (${ capitalize ( defaults . agentEngine ) } )` : 'Default' ;
114-
115101 return (
116102 < TooltipProvider >
117103 < div className = "max-w-2xl space-y-6" >
@@ -122,38 +108,16 @@ export function ProjectHarnessForm({ project }: { project: Project }) {
122108 </ p >
123109 </ div >
124110
125- { /* Engine & Runtime Card */ }
111+ { /* Model & Iterations Card — engine-agnostic, always visible */ }
126112 < Card >
127113 < CardHeader >
128- < CardTitle > Engine & Runtime</ CardTitle >
114+ < CardTitle > Model & Runtime</ CardTitle >
129115 < CardDescription >
130- Choose which AI engine runs agents and configure its parameters .
116+ Global model and iteration settings applied to all agents unless overridden per-agent .
131117 </ CardDescription >
132118 </ CardHeader >
133119 < CardContent >
134120 < form onSubmit = { handleSubmit } className = "space-y-4" id = "engine-runtime-form" >
135- < div className = "space-y-2" >
136- < Label > Agent Engine</ Label >
137- < Select
138- value = { agentEngine || '_none' }
139- onValueChange = { ( v ) => setAgentEngine ( v === '_none' ? '' : v ) }
140- >
141- < SelectTrigger className = "w-full" >
142- < SelectValue placeholder = { defaultEngineLabel } />
143- </ SelectTrigger >
144- < SelectContent >
145- < SelectItem value = "_none" > { defaultEngineLabel } </ SelectItem >
146- { enginesQuery . data ?. map ( ( engine ) => (
147- < SelectItem key = { engine . id } value = { engine . id } >
148- { engine . label }
149- </ SelectItem >
150- ) ) }
151- </ SelectContent >
152- </ Select >
153- < p className = "text-xs text-muted-foreground" >
154- Determines which AI SDK processes agent runs.
155- </ p >
156- </ div >
157121 < div className = "space-y-2" >
158122 < div className = "flex items-center gap-1.5" >
159123 < Label htmlFor = "model" > Model</ Label >
@@ -178,12 +142,6 @@ export function ProjectHarnessForm({ project }: { project: Project }) {
178142 Project default model. Per-agent overrides in the Agents tab.
179143 </ p >
180144 </ div >
181- < EngineSettingsFields
182- engine = { effectiveEngine }
183- value = { engineSettings }
184- onChange = { ( next ) => setEngineSettings ( next ?? { } ) }
185- engineDefaults = { engineDefaults }
186- />
187145 < div className = "space-y-2" >
188146 < div className = "flex items-center gap-1.5" >
189147 < Label htmlFor = "maxIterations" > Max Iterations</ Label >
@@ -212,6 +170,157 @@ export function ProjectHarnessForm({ project }: { project: Project }) {
212170 </ div >
213171 </ form >
214172 </ CardContent >
173+ </ Card >
174+
175+ { /* Per-engine tabs: credentials + settings + default toggle */ }
176+ < Card >
177+ < CardHeader >
178+ < CardTitle > Engine Settings & Credentials</ CardTitle >
179+ < CardDescription >
180+ Configure each engine's credentials and settings. The default engine tab is
181+ highlighted. New engines are added automatically as the catalog expands.
182+ </ CardDescription >
183+ </ CardHeader >
184+ < CardContent >
185+ { engines . length === 0 ? (
186+ < p className = "text-sm text-muted-foreground" > Loading engines…</ p >
187+ ) : (
188+ < Tabs defaultValue = { defaultTab } >
189+ < TabsList className = "flex w-full h-auto flex-wrap" >
190+ { engines . map ( ( engine ) => {
191+ const isDefault = engine . id === effectiveEngineId ;
192+ const isUsedByAgents = agentEnginesInUse . includes ( engine . id ) ;
193+ return (
194+ < TabsTrigger
195+ key = { engine . id }
196+ value = { engine . id }
197+ className = "flex items-center gap-1.5"
198+ >
199+ { engine . label }
200+ { isDefault && (
201+ < Badge variant = "secondary" className = "text-xs px-1 py-0" >
202+ Default
203+ </ Badge >
204+ ) }
205+ { ! isDefault && isUsedByAgents && (
206+ < Badge variant = "outline" className = "text-xs px-1 py-0" >
207+ In use
208+ </ Badge >
209+ ) }
210+ </ TabsTrigger >
211+ ) ;
212+ } ) }
213+ </ TabsList >
214+
215+ { engines . map ( ( engine ) => {
216+ const isDefault = engine . id === effectiveEngineId ;
217+ const isUsedByAgents = agentEnginesInUse . includes ( engine . id ) ;
218+ const engineSecrets = ENGINE_SECRETS . filter ( ( s ) =>
219+ s . engines ?. includes ( engine . id ) ,
220+ ) ;
221+ // Secrets shared with other engines: show a note
222+ const sharedSecretEngines = ( envVarKey : string ) : string [ ] => {
223+ const secret = ENGINE_SECRETS . find ( ( s ) => s . envVarKey === envVarKey ) ;
224+ if ( ! secret ?. engines ) return [ ] ;
225+ return secret . engines . filter ( ( e ) => e !== engine . id ) ;
226+ } ;
227+
228+ const engineDefaults = getEngineDefaults ( engine . id ) ;
229+
230+ return (
231+ < TabsContent key = { engine . id } value = { engine . id } className = "mt-4 space-y-6" >
232+ { /* Engine description */ }
233+ { engine . description && (
234+ < p className = "text-sm text-muted-foreground" > { engine . description } </ p >
235+ ) }
236+
237+ { /* Default engine indicator / Set as Default button */ }
238+ < div className = "flex items-center gap-3" >
239+ { isDefault ? (
240+ < div className = "flex items-center gap-2 rounded-md border border-border bg-muted/50 px-3 py-2 text-sm" >
241+ < span className = "text-muted-foreground" >
242+ ✓ Default engine for this project
243+ { agentEngine === '' &&
244+ ` (inheriting system default: ${ capitalize ( systemDefaultEngineId ) } )` }
245+ </ span >
246+ { agentEngine !== '' && (
247+ < button
248+ type = "button"
249+ onClick = { ( ) => setAgentEngine ( '' ) }
250+ className = "ml-2 text-xs text-muted-foreground underline hover:text-foreground transition-colors"
251+ >
252+ Reset to system default
253+ </ button >
254+ ) }
255+ </ div >
256+ ) : (
257+ < button
258+ type = "button"
259+ onClick = { ( ) => setAgentEngine ( engine . id ) }
260+ className = "inline-flex h-9 items-center rounded-md border border-input bg-background px-4 text-sm font-medium hover:bg-accent hover:text-accent-foreground transition-colors"
261+ >
262+ Set as Default Engine
263+ </ button >
264+ ) }
265+ { ! isDefault && isUsedByAgents && (
266+ < span className = "text-xs text-muted-foreground" >
267+ Used by agent config overrides
268+ </ span >
269+ ) }
270+ </ div >
271+
272+ { /* Engine settings */ }
273+ < EngineSettingsFields
274+ engine = { engine }
275+ value = { engineSettings }
276+ onChange = { ( next ) => setEngineSettings ( next ?? { } ) }
277+ engineDefaults = { engineDefaults }
278+ />
279+
280+ { /* Engine credentials */ }
281+ { engineSecrets . length > 0 ? (
282+ < div className = "space-y-4" >
283+ < div >
284+ < h4 className = "text-sm font-medium" > Credentials</ h4 >
285+ < p className = "text-xs text-muted-foreground mt-0.5" >
286+ API keys and tokens for { engine . label } . Values are stored encrypted
287+ and never returned to the browser.
288+ </ p >
289+ </ div >
290+ { engineSecrets . map ( ( secret ) => {
291+ const sharedWith = sharedSecretEngines ( secret . envVarKey ) ;
292+ const sharedNote =
293+ sharedWith . length > 0
294+ ? `Also used by: ${ sharedWith . map ( ( id ) => engines . find ( ( e ) => e . id === id ) ?. label ?? id ) . join ( ', ' ) } `
295+ : undefined ;
296+ const description =
297+ secret . description + ( sharedNote ? ` · ${ sharedNote } ` : '' ) ;
298+ return (
299+ < ProjectSecretField
300+ key = { secret . envVarKey }
301+ projectId = { project . id }
302+ envVarKey = { secret . envVarKey }
303+ label = { secret . label }
304+ description = { description }
305+ placeholder = { secret . placeholder }
306+ credential = { credentials . find (
307+ ( c ) => c . envVarKey === secret . envVarKey ,
308+ ) }
309+ />
310+ ) ;
311+ } ) }
312+ </ div >
313+ ) : (
314+ < p className = "text-sm text-muted-foreground" >
315+ No credentials required for { engine . label } .
316+ </ p >
317+ ) }
318+ </ TabsContent >
319+ ) ;
320+ } ) }
321+ </ Tabs >
322+ ) }
323+ </ CardContent >
215324 < CardFooter >
216325 < div className = "flex items-center gap-2" >
217326 < button
@@ -231,48 +340,6 @@ export function ProjectHarnessForm({ project }: { project: Project }) {
231340 </ div >
232341 </ CardFooter >
233342 </ Card >
234-
235- { /* Engine Credentials Card */ }
236- < Card >
237- < CardHeader >
238- < CardTitle > Engine Credentials</ CardTitle >
239- < CardDescription >
240- API keys and tokens for the agent engine. Values are stored encrypted and never
241- returned to the browser.
242- </ CardDescription >
243- </ CardHeader >
244- < CardContent >
245- { allEnginesInUse . length === 0 ? (
246- < p className = "text-sm text-muted-foreground" >
247- Select an engine above to see required credentials.
248- </ p >
249- ) : visibleSecrets . length === 0 ? (
250- < p className = "text-sm text-muted-foreground" >
251- No credentials required for the selected engine.
252- </ p >
253- ) : (
254- < div className = "space-y-4" >
255- { agentEnginesInUse . length > 0 && effectiveEngineId && (
256- < p className = "text-xs text-muted-foreground" >
257- Showing credentials for the project engine and engines used by individual agent
258- configs.
259- </ p >
260- ) }
261- { visibleSecrets . map ( ( secret ) => (
262- < ProjectSecretField
263- key = { secret . envVarKey }
264- projectId = { project . id }
265- envVarKey = { secret . envVarKey }
266- label = { secret . label }
267- description = { secret . description }
268- placeholder = { secret . placeholder }
269- credential = { credentials . find ( ( c ) => c . envVarKey === secret . envVarKey ) }
270- />
271- ) ) }
272- </ div >
273- ) }
274- </ CardContent >
275- </ Card >
276343 </ div >
277344 </ TooltipProvider >
278345 ) ;
0 commit comments