diff --git a/web/src/components/projects/project-harness-form.tsx b/web/src/components/projects/project-harness-form.tsx index b895cc35..abf65152 100644 --- a/web/src/components/projects/project-harness-form.tsx +++ b/web/src/components/projects/project-harness-form.tsx @@ -17,6 +17,13 @@ import { } from '@/components/ui/card.js'; import { Input } from '@/components/ui/input.js'; import { Label } from '@/components/ui/label.js'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select.js'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs.js'; import { Tooltip, @@ -74,8 +81,9 @@ export function ProjectHarnessForm({ project }: { project: Project }) { // The effective project-level engine: either explicitly set or the system default const effectiveEngineId = agentEngine || systemDefaultEngineId; - // Default tab to show: project's selected engine, or system default - const defaultTab = effectiveEngineId; + // Controlled active tab — null means "follow effectiveEngineId reactively" (handles async defaultsQuery) + const [activeTab, setActiveTab] = useState(null); + const currentTab = activeTab ?? effectiveEngineId; // Resolved engine defaults for EngineSettingsFields function getEngineDefaults(engineId: string): Record | undefined { @@ -84,6 +92,14 @@ export function ProjectHarnessForm({ project }: { project: Project }) { : undefined; } + function handleEngineSelectChange(value: string) { + const newEngine = value === '_system' ? '' : value; + setAgentEngine(newEngine); + // Switch active tab to the newly selected default engine + const newEffective = newEngine || systemDefaultEngineId; + setActiveTab(newEffective); + } + function handleSubmit(e: React.FormEvent) { e.preventDefault(); const activeEngine = agentEngine || null; @@ -107,224 +123,208 @@ export function ProjectHarnessForm({ project }: { project: Project }) {

- {/* Model & Iterations Card — engine-agnostic, always visible */} - Model & Runtime + Engine - Global model and iteration settings applied to all agents unless overridden per-agent. + Choose the default engine, then configure its model, settings, and credentials. -
-
-
- - - - - - - Individual agents can override this in the Agents tab. - - -
- -

- Project default model. Per-agent overrides in the Agents tab. -

-
+ + {/* Default Engine Selector */}
-
- - - - - - - Individual agents can override this in the Agents tab. - - -
- setMaxIterations(e.target.value)} - placeholder={defaults ? `${defaults.maxIterations} (default)` : 'e.g. 50'} - /> + + {engines.length === 0 ? ( +

Loading engines…

+ ) : ( + + )}

- Safety limit on tool-call iterations per run. + Used by all agents unless overridden per-agent.

-
-
-
- {/* Per-engine tabs: credentials + settings + default toggle */} - - - Engine Settings & Credentials - - Configure each engine's credentials and settings. The default engine tab is - highlighted. New engines are added automatically as the catalog expands. - - - - {engines.length === 0 ? ( -

Loading engines…

- ) : ( - - + {/* Per-engine configuration tabs */} + {engines.length > 0 && ( + + + {engines.map((engine) => { + const isDefault = engine.id === effectiveEngineId; + const isUsedByAgents = agentEnginesInUse.includes(engine.id); + return ( + + {engine.label} + {isDefault && ( + + Default + + )} + {!isDefault && isUsedByAgents && ( + + In use + + )} + + ); + })} + + {engines.map((engine) => { const isDefault = engine.id === effectiveEngineId; - const isUsedByAgents = agentEnginesInUse.includes(engine.id); - return ( - - {engine.label} - {isDefault && ( - - Default - - )} - {!isDefault && isUsedByAgents && ( - - In use - - )} - + const engineSecrets = ENGINE_SECRETS.filter((s) => + s.engines?.includes(engine.id), ); - })} - - - {engines.map((engine) => { - const isDefault = engine.id === effectiveEngineId; - const isUsedByAgents = agentEnginesInUse.includes(engine.id); - const engineSecrets = ENGINE_SECRETS.filter((s) => - s.engines?.includes(engine.id), - ); - // Secrets shared with other engines: show a note - const sharedSecretEngines = (envVarKey: string): string[] => { - const secret = ENGINE_SECRETS.find((s) => s.envVarKey === envVarKey); - if (!secret?.engines) return []; - return secret.engines.filter((e) => e !== engine.id); - }; + const sharedSecretEngines = (envVarKey: string): string[] => { + const secret = ENGINE_SECRETS.find((s) => s.envVarKey === envVarKey); + if (!secret?.engines) return []; + return secret.engines.filter((e) => e !== engine.id); + }; + const engineDefaults = getEngineDefaults(engine.id); - const engineDefaults = getEngineDefaults(engine.id); - - return ( - - {/* Engine description */} - {engine.description && ( -

{engine.description}

- )} + return ( + + {/* Engine description */} + {engine.description && ( +

{engine.description}

+ )} - {/* Default engine indicator / Set as Default button */} -
- {isDefault ? ( -
- - ✓ Default engine for this project - {agentEngine === '' && - ` (inheriting system default: ${capitalize(systemDefaultEngineId)})`} - - {agentEngine !== '' && ( - - )} + {/* Model — only shown for the default engine (project-level setting) */} + {isDefault && ( +
+
+ + + + + + + Individual agents can override this in the Agents tab. + + +
+ +

+ Project default. Per-agent overrides in the Agents tab. +

- ) : ( - - )} - {!isDefault && isUsedByAgents && ( - - Used by agent config overrides - )} -
- {/* Engine settings */} - setEngineSettings(next ?? {})} - engineDefaults={engineDefaults} - /> + {/* Engine Settings */} + setEngineSettings(next ?? {})} + engineDefaults={engineDefaults} + /> - {/* Engine credentials */} - {engineSecrets.length > 0 ? ( -
-
-

Credentials

-

- API keys and tokens for {engine.label}. Values are stored encrypted - and never returned to the browser. + {/* Max Iterations — only shown for the default engine (project-level setting) */} + {isDefault && ( +

+
+ + + + + + + Individual agents can override this in the Agents tab. + + +
+ setMaxIterations(e.target.value)} + placeholder={ + defaults ? `${defaults.maxIterations} (default)` : 'e.g. 50' + } + /> +

+ Safety limit on tool-call iterations per run.

- {engineSecrets.map((secret) => { - const sharedWith = sharedSecretEngines(secret.envVarKey); - const sharedNote = - sharedWith.length > 0 - ? `Also used by: ${sharedWith.map((id) => engines.find((e) => e.id === id)?.label ?? id).join(', ')}` - : undefined; - const description = - secret.description + (sharedNote ? ` · ${sharedNote}` : ''); - return ( - c.envVarKey === secret.envVarKey, - )} - /> - ); - })} -
- ) : ( -

- No credentials required for {engine.label}. -

- )} - - ); - })} - - )} + )} + + {/* Credentials */} + {engineSecrets.length > 0 ? ( +
+
+

Credentials

+

+ API keys and tokens for {engine.label}. Values are stored encrypted + and never returned to the browser. +

+
+ {engineSecrets.map((secret) => { + const sharedWith = sharedSecretEngines(secret.envVarKey); + const sharedNote = + sharedWith.length > 0 + ? `Also used by: ${sharedWith.map((id) => engines.find((e) => e.id === id)?.label ?? id).join(', ')}` + : undefined; + const description = + secret.description + (sharedNote ? ` · ${sharedNote}` : ''); + return ( + c.envVarKey === secret.envVarKey, + )} + /> + ); + })} +
+ ) : ( +

+ No credentials required for {engine.label}. +

+ )} + + ); + })} + + )} +