Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions desktop/src/features/agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,27 @@ with a TypeScript lookup table or an id comparison in a component.
harnesses always keep the field. Gate: `defaults hides model when optional
harness has empty discovery` (and the failed-discovery counterpart) in
`onboarding-agent-defaults.spec.ts`.
9. **The defaults modal is progressively disclosed.** An unset global config
starts on the Buzz Agent-first deployment fallback and carries that visible
harness into the next saved edit. The `progressive-defaults` disclosure
preset therefore begins at Provider for Buzz Agent, then reveals Model,
Effort, and Advanced only after a provider is configured. Harnesses whose
runtime metadata has no provider field skip that gate. Reveals animate their
height through Motion and become immediate when reduced motion is requested.
Once the Advanced toggle is visible, its expanded state is exclusively
user-controlled: provider, harness, and required-env changes must never
open it automatically in defaults, create, or edit flows. In Create mode,
the defaults summary follows preferred-harness changes saved while the
dialog is open, and its configured state includes required credentials as
well as provider/model values. If no available harness can resolve, Create
starts in Customize and lets unavailable catalog entries be selected only
to expose their setup guidance; submission remains blocked.
Advanced-only required credentials mark the collapsed Advanced toggle
without opening it in Global Defaults and Edit, and block incomplete saves.
Runtime-file credentials satisfy Global Defaults just as they do Create and
Edit. In Edit,
selecting Custom command keeps its required command field beside the harness
picker rather than hiding it in Advanced.

## The tests that enforce this

Expand Down
26 changes: 26 additions & 0 deletions desktop/src/features/agents/ui/AdvancedRequiredBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { hasMissingRequiredEnvKey } from "./personaRuntimeModel";

export function AdvancedRequiredBadge({
envVars,
requiredEnvKeys,
show,
testId,
}: {
envVars?: Record<string, string>;
requiredEnvKeys?: readonly string[];
show?: boolean;
testId: string;
}) {
const visible =
show ?? hasMissingRequiredEnvKey(requiredEnvKeys ?? [], envVars ?? {});
if (!visible) return null;
return (
<span
aria-hidden="true"
className="rounded-full bg-destructive/10 px-2 py-0.5 text-xs text-destructive"
data-testid={testId}
>
Required
</span>
);
}
86 changes: 71 additions & 15 deletions desktop/src/features/agents/ui/AgentAiConfigurationMode.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,67 @@
import type * as React from "react";
import { Tabs, TabsList, TabsTrigger } from "@/shared/ui/tabs";
import type { AgentAiConfigurationMode } from "./agentAiConfigurationPolicy";
import { AgentAiDefaultsNotice } from "./AgentAiDefaults";
import type { InheritedDefault } from "./bakedEnvHelpers";

export type { AgentAiConfigurationMode } from "./agentAiConfigurationPolicy";

export function HarnessModelDefaultNotice({
harness,
model,
}: {
harness: string;
model?: string | null;
}) {
return (
<div className="text-sm" data-testid="agent-harness-defaults-notice">
<span className="text-muted-foreground">Model</span>{" "}
<span className="text-foreground">
<dl
className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 text-sm"
data-testid="agent-harness-defaults-notice"
>
<dt className="text-muted-foreground">Harness</dt>
<dd className="truncate text-foreground">
{harness || "Not configured"}
</dd>
<dt className="text-muted-foreground">Model</dt>
<dd className="truncate text-foreground">
{model?.trim() || "Harness default"}
</span>
</div>
</dd>
</dl>
);
}

export function AgentCreateAiDefaultsSummary({
canChooseProvider,
harness,
inheritedModel,
inheritedProvider,
isConfigured,
model,
onEditDefaults,
triggerRef,
}: {
canChooseProvider: boolean;
harness: string;
inheritedModel: InheritedDefault;
inheritedProvider: InheritedDefault;
isConfigured: boolean;
model?: string | null;
onEditDefaults: () => void;
triggerRef?: React.Ref<HTMLButtonElement>;
}) {
return canChooseProvider ? (
<AgentAiDefaultsNotice
isConfigured={isConfigured}
onEditDefaults={onEditDefaults}
triggerRef={triggerRef}
explicitModel=""
explicitProvider=""
harness={harness}
inheritedModel={inheritedModel}
inheritedProvider={inheritedProvider}
/>
) : (
<HarnessModelDefaultNotice harness={harness} model={model} />
);
}

Expand All @@ -36,22 +83,31 @@ export function AgentAiConfigurationModeField({
}
value={mode}
>
<TabsList>
<TabsTrigger value="defaults">
<TabsList className="relative isolate grid h-9 w-full grid-cols-2 overflow-hidden rounded-lg bg-muted p-0.5">
<div
aria-hidden="true"
className="absolute bottom-0.5 left-0.5 top-0.5 z-0 rounded-md bg-background shadow-sm transition-transform duration-[250ms] ease-out"
style={{
transform: `translateX(${mode === "custom" ? 100 : 0}%)`,
width: "calc((100% - 4px) / 2)",
}}
/>
<TabsTrigger
className="relative z-10 h-full rounded-md bg-transparent text-xs font-medium shadow-none transition-colors data-[state=active]:bg-transparent data-[state=active]:shadow-none"
value="defaults"
>
{needsProviderSelection
? "Use agent defaults"
: "Use harness defaults"}
</TabsTrigger>
<TabsTrigger value="custom">Customize for this agent</TabsTrigger>
<TabsTrigger
className="relative z-10 h-full rounded-md bg-transparent text-xs font-medium shadow-none transition-colors data-[state=active]:bg-transparent data-[state=active]:shadow-none"
value="custom"
>
Customize for this agent
</TabsTrigger>
</TabsList>
</Tabs>
{mode === "custom" ? (
<p className="text-xs text-muted-foreground">
{needsProviderSelection
? "Provider and model changes apply only to this agent."
: "Model changes apply only to this agent."}
</p>
) : null}
</div>
);
}
36 changes: 36 additions & 0 deletions desktop/src/features/agents/ui/AgentAiDefaults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,62 @@ export function formatAiDefaultsSummary({
}

export function AgentAiDefaultsNotice({
isConfigured = true,
onEditDefaults,
triggerRef,
explicitModel,
explicitProvider,
harness,
inheritedModel,
inheritedProvider,
}: {
isConfigured?: boolean;
onEditDefaults: () => void;
triggerRef?: React.Ref<HTMLButtonElement>;
explicitModel: string;
explicitProvider: string;
harness?: string;
inheritedModel: InheritedDefault;
inheritedProvider: InheritedDefault;
}) {
const provider = explicitProvider.trim() || inheritedProvider.value;
const model = explicitModel.trim() || inheritedModel.value;

if (!isConfigured) {
return (
<div
className="flex items-center justify-between gap-3 rounded-xl border border-border/70 bg-muted/30 px-3 py-2.5"
data-testid="agent-ai-defaults-notice"
>
<p className="text-sm font-medium text-foreground">
Global defaults not set
</p>
<Button
className="shrink-0"
data-testid="set-ai-defaults"
onClick={onEditDefaults}
ref={triggerRef}
size="sm"
type="button"
variant="outline"
>
Set
</Button>
</div>
);
}

return (
<div className="space-y-1" data-testid="agent-ai-defaults-notice">
<dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 text-sm">
{harness !== undefined ? (
<>
<dt className="text-muted-foreground">Harness</dt>
<dd className="truncate text-foreground">
{harness || "Not configured"}
</dd>
</>
) : null}
<dt className="text-muted-foreground">Provider</dt>
<dd className="truncate text-foreground">
{provider ? providerLabel(provider) : "Not configured"}
Expand Down
Loading
Loading