Skip to content

Commit 04e71e4

Browse files
aaightCascade Botclaude
authored
feat(dashboard): per-engine tabs with credentials, settings, and default indicator (#1057)
* feat(dashboard): per-engine tabs with credentials, settings, and default indicator * fix(dashboard): address review feedback on engine credential descriptions - Remove dead code: `defaultEngineLabel` variable left over after Select dropdown was removed - Combine original secret description with sharing note instead of replacing it - Map shared engine IDs to human-readable labels (engine.label) in "Also used by" note Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(deps): upgrade picomatch to 4.0.4 to resolve high-severity audit vulnerabilities Fixes two high-severity CVEs in picomatch 4.0.3: - GHSA-c2c7-rcm5-vvqj: ReDoS vulnerability via extglob quantifiers - GHSA-3v7f-55p6-f55p: Method Injection in POSIX Character Classes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(dashboard): add reset-to-default affordance and remove duplicate save button - Add "Reset to system default" link on the default engine tab when an engine is explicitly set, calling setAgentEngine('') to restore inherit behaviour - Remove the duplicate Save Changes footer from the Model & Runtime card; a single save button in the Engine Settings & Credentials card footer now covers both cards via form="engine-runtime-form" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Cascade Bot <bot@cascade.dev> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 64d9851 commit 04e71e4

2 files changed

Lines changed: 180 additions & 113 deletions

File tree

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/src/components/projects/project-harness-form.tsx

Lines changed: 177 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ProjectSecretField } from '@/components/projects/project-secret-field.j
33
import { useProjectUpdate } from '@/components/projects/use-project-update.js';
44
import { EngineSettingsFields } from '@/components/settings/engine-settings-fields.js';
55
import { ModelField } from '@/components/settings/model-field.js';
6+
import { Badge } from '@/components/ui/badge.js';
67
import {
78
Card,
89
CardContent,
@@ -13,13 +14,7 @@ import {
1314
} from '@/components/ui/card.js';
1415
import { Input } from '@/components/ui/input.js';
1516
import { 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';
2318
import {
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
5146
export 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 &amp; Runtime</CardTitle>
114+
<CardTitle>Model &amp; 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 &amp; 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

Comments
 (0)