diff --git a/package-lock.json b/package-lock.json index d91a35b3..48db4d53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8603,9 +8603,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", "engines": { "node": ">=12" diff --git a/web/src/components/projects/project-harness-form.tsx b/web/src/components/projects/project-harness-form.tsx index a7921e0b..31f3e085 100644 --- a/web/src/components/projects/project-harness-form.tsx +++ b/web/src/components/projects/project-harness-form.tsx @@ -3,6 +3,7 @@ import { ProjectSecretField } from '@/components/projects/project-secret-field.j import { useProjectUpdate } from '@/components/projects/use-project-update.js'; import { EngineSettingsFields } from '@/components/settings/engine-settings-fields.js'; import { ModelField } from '@/components/settings/model-field.js'; +import { Badge } from '@/components/ui/badge.js'; import { Card, CardContent, @@ -13,13 +14,7 @@ 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, TooltipContent, @@ -47,7 +42,7 @@ function capitalize(s: string): string { return s.charAt(0).toUpperCase() + s.slice(1); } -// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: multiple query dependencies and credential sections for engine-specific settings rendering +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: multiple query dependencies and per-engine tab rendering for credentials and settings export function ProjectHarnessForm({ project }: { project: Project }) { const updateMutation = useProjectUpdate(project.id); const enginesQuery = useQuery(trpc.agentConfigs.engines.queryOptions()); @@ -70,48 +65,39 @@ export function ProjectHarnessForm({ project }: { project: Project }) { project.engineSettings ?? {}, ); - const effectiveEngineId = agentEngine || ''; - const effectiveEngine = enginesQuery.data?.find((engine) => engine.id === effectiveEngineId); + // Derived values + const engines = enginesQuery.data ?? []; + const credentials = credentialsQuery.data ?? []; + const agentEnginesInUse = enginesInUseQuery.data ?? []; + + // System default engine (e.g. 'claude-code') shown when no project-level engine is set + const systemDefaultEngineId = defaults?.agentEngine ?? 'claude-code'; + // 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; - // Resolved engine defaults for the EngineSettingsFields component - const engineDefaults = - defaults && effectiveEngineId - ? (defaults.engineSettings as Record>)[effectiveEngineId] + // Resolved engine defaults for EngineSettingsFields + function getEngineDefaults(engineId: string): Record | undefined { + return defaults + ? (defaults.engineSettings as Record>)[engineId] : undefined; + } function handleSubmit(e: React.FormEvent) { e.preventDefault(); const activeEngine = agentEngine || null; - const activeEngineSettings = - activeEngine && engineSettings[activeEngine] - ? { [activeEngine]: engineSettings[activeEngine] } - : null; + // Save all engine settings, not just the active engine + const allEngineSettings = Object.keys(engineSettings).length > 0 ? engineSettings : null; updateMutation.mutate({ model: model || null, maxIterations: maxIterations ? Number.parseInt(maxIterations, 10) : null, agentEngine: activeEngine, - engineSettings: activeEngineSettings, + engineSettings: allEngineSettings, }); } - const credentials = credentialsQuery.data ?? []; - - // Collect all engine IDs that need credentials: - // 1. The project-level selected engine (effectiveEngineId) - // 2. Any per-agent engine overrides from agent configs - const agentEnginesInUse = enginesInUseQuery.data ?? []; - const allEnginesInUse = effectiveEngineId - ? Array.from(new Set([effectiveEngineId, ...agentEnginesInUse])) - : agentEnginesInUse; - - // Show engine secrets for all engines in use (grouped by engine when multiple) - const visibleSecrets = ENGINE_SECRETS.filter( - (s) => !s.engines || s.engines.some((e) => allEnginesInUse.includes(e)), - ); - - // Default engine label for the select placeholder - const defaultEngineLabel = defaults ? `Default (${capitalize(defaults.agentEngine)})` : 'Default'; - return (
@@ -122,38 +108,16 @@ export function ProjectHarnessForm({ project }: { project: Project }) {

- {/* Engine & Runtime Card */} + {/* Model & Iterations Card — engine-agnostic, always visible */} - Engine & Runtime + Model & Runtime - Choose which AI engine runs agents and configure its parameters. + Global model and iteration settings applied to all agents unless overridden per-agent.
-
- - -

- Determines which AI SDK processes agent runs. -

-
@@ -178,12 +142,6 @@ export function ProjectHarnessForm({ project }: { project: Project }) { Project default model. Per-agent overrides in the Agents tab.

- setEngineSettings(next ?? {})} - engineDefaults={engineDefaults} - />
@@ -212,6 +170,157 @@ export function ProjectHarnessForm({ project }: { project: Project }) {
+ + + {/* 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…

+ ) : ( + + + {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); + 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 engineDefaults = getEngineDefaults(engine.id); + + 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 !== '' && ( + + )} +
+ ) : ( + + )} + {!isDefault && isUsedByAgents && ( + + Used by agent config overrides + + )} +
+ + {/* 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. +

+
+ {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}. +

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