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
104 changes: 104 additions & 0 deletions desktop/src/features/agents/ui/AgentAiDefaults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
import type { InheritedDefault } from "./bakedEnvHelpers";
import { getPersonaProviderOptions } from "./personaDialogPickers";
import { Button } from "@/shared/ui/button";

function providerLabel(providerId: string) {
const option = getPersonaProviderOptions("", "buzz-agent").find(
(candidate) => candidate.id === providerId,
);
return option?.label ?? providerId;
}

export function formatAiDefaultsSummary({
provider,
model,
}: {
provider: InheritedDefault;
model: InheritedDefault;
}) {
const parts = [
provider.value ? providerLabel(provider.value) : null,
model.value || null,
].filter((value): value is string => Boolean(value));

return parts.length > 0 ? parts.join(" · ") : "Not configured";
}

export function AgentAiDefaultsNotice({
confirmNavigation = false,
explicitModel,
explicitProvider,
inheritedModel,
inheritedProvider,
}: {
confirmNavigation?: boolean;
explicitModel: string;
explicitProvider: string;
inheritedModel: InheritedDefault;
inheritedProvider: InheritedDefault;
}) {
const { goSettings } = useAppNavigation();
const inheritsProvider = explicitProvider.trim().length === 0;
const inheritsModel = explicitModel.trim().length === 0;

const usesCustomConfig = !inheritsProvider && !inheritsModel;
const requiredProviderMissing = inheritsProvider && !inheritedProvider.value;

const inheritedParts = [
inheritsProvider
? inheritedProvider.value
? `Provider ${providerLabel(inheritedProvider.value)}`
: "Provider not configured"
: null,
inheritsModel
? inheritedModel.value
? `Model ${inheritedModel.value}`
: "Model not configured"
: null,
].filter((value): value is string => Boolean(value));

return (
<div
className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-border/60 bg-muted/30 px-3 py-2"
data-testid="agent-ai-defaults-notice"
>
<div className="min-w-0">
<p className="text-xs font-medium text-foreground">
{usesCustomConfig
? "Custom AI configuration"
: requiredProviderMissing
? "AI defaults aren’t configured"
: inheritsProvider && inheritsModel
? "Uses AI defaults"
: "Partially uses AI defaults"}
</p>
<p className="text-xs text-muted-foreground">
{usesCustomConfig
? "This agent won’t follow provider or model default changes."
: requiredProviderMissing
? "Choose a provider in AI defaults to use this agent."
: `${inheritedParts.join(" · ")}. Inherited fields follow future changes.`}
</p>
</div>
<Button
onClick={() => {
if (
confirmNavigation &&
!window.confirm(
"Leave this agent without saving? Your changes will be discarded.",
)
) {
return;
}
void goSettings("agents");
}}
size="xs"
type="button"
variant="link"
>
Edit AI defaults
</Button>
</div>
);
}
9 changes: 9 additions & 0 deletions desktop/src/features/agents/ui/AgentDefinitionDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import {
getBakedProviderInheritLabel,
} from "./bakedEnvHelpers";
import { useAgentDialogDefaults } from "./useAgentDialogDefaults";
import { AgentAiDefaultsNotice } from "./AgentAiDefaults";
import { useProviderApiKeyFieldState } from "./providerApiKeyFieldState";
import { buildRuntimeModelProviderPayload } from "./agentDefinitionSubmitPayload";

Expand Down Expand Up @@ -864,6 +865,14 @@ export function AgentDefinitionDialog({
) : null}
</AnimatePresence>

<AgentAiDefaultsNotice
confirmNavigation
explicitModel={model}
explicitProvider={provider}
inheritedModel={inheritedModelDefault}
inheritedProvider={inheritedProviderDefault}
/>

{isCreateMode ? createRunSection : null}

<div className="space-y-3">
Expand Down
9 changes: 9 additions & 0 deletions desktop/src/features/agents/ui/AgentInstanceEditDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {
} from "./bakedEnvHelpers";
import { getProviderApiKeyEnvVar } from "./personaDialogPickers";
import { useAgentDialogDefaults } from "./useAgentDialogDefaults";
import { AgentAiDefaultsNotice } from "./AgentAiDefaults";
import { useProviderApiKeyFieldState } from "./providerApiKeyFieldState";

const ADVANCED_FIELDS_MOTION_TRANSITION = {
Expand Down Expand Up @@ -1087,6 +1088,14 @@ export function AgentInstanceEditDialog({
</p>
</div>

<AgentAiDefaultsNotice
confirmNavigation
explicitModel={inheritedSubmission.model ?? ""}
explicitProvider={inheritedSubmission.provider ?? ""}
inheritedModel={inheritedModelDefault}
inheritedProvider={inheritedProviderDefault}
/>

{/* Advanced settings */}
<div className="space-y-3">
<button
Expand Down
17 changes: 14 additions & 3 deletions desktop/src/features/agents/ui/AgentsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,20 @@ import { useManagedAgentActions } from "./useManagedAgentActions";
import { usePersonaActions } from "./usePersonaActions";
import { useTeamActions } from "./useTeamActions";
import { useProfilePanel } from "@/shared/context/ProfilePanelContext";
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
import { useBakedBuildEnvQuery } from "@/features/agents/hooks";
import { useGlobalAgentConfig } from "@/features/agents/useGlobalAgentConfig";
import { PageHeader } from "@/shared/ui/PageHeader";
import { GlobalAgentConfigSettingsCard } from "@/features/settings/ui/GlobalAgentConfigSettingsCard";
import { formatAiDefaultsSummary } from "./AgentAiDefaults";
import { getInheritedAgentDefaults } from "./bakedEnvHelpers";

export function AgentsView() {
const { openPersonaProfilePanel, openProfilePanel } = useProfilePanel();
const { goSettings } = useAppNavigation();
const { globalConfig } = useGlobalAgentConfig();
const { data: bakedEnv } = useBakedBuildEnvQuery({ enabled: true });
const inheritedDefaults = getInheritedAgentDefaults(globalConfig, bakedEnv);
const aiDefaultsSummary = formatAiDefaultsSummary(inheritedDefaults);
const agents = useManagedAgentActions();
const personas = usePersonaActions();
const teamImportInputRef = React.useRef<HTMLInputElement | null>(null);
Expand Down Expand Up @@ -95,9 +104,11 @@ export function AgentsView() {
title="Agents"
/>
<div className="flex flex-col gap-8">
<GlobalAgentConfigSettingsCard />

<UnifiedAgentsSection
aiDefaultsSummary={aiDefaultsSummary}
onEditAiDefaults={() => {
void goSettings("agents");
}}
actionErrorMessage={agents.actionErrorMessage}
actionNoticeMessage={agents.actionNoticeMessage}
agents={agents.managedAgents}
Expand Down
27 changes: 26 additions & 1 deletion desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { PersonaActionsMenu } from "./PersonaActionsMenu";
import { buildUnifiedGroups, pickProfileAgent } from "./unifiedAgentGroups";

type UnifiedAgentsSectionProps = {
aiDefaultsSummary: string;
actionErrorMessage: string | null;
actionNoticeMessage: string | null;
agents: ManagedAgent[];
Expand All @@ -41,6 +42,7 @@ type UnifiedAgentsSectionProps = {
startingAgentPubkey: string | null;
startingPersonaIds: ReadonlySet<string>;
onBulkStopRunning: () => void;
onEditAiDefaults: () => void;
onOpenAgentProfile: (
pubkey: string,
options?: ProfilePanelOpenOptions,
Expand Down Expand Up @@ -74,6 +76,7 @@ const AGENT_CARD_GRID_CLASS = `${AGENT_CARD_COLUMN_CLASS} grid grid-cols-[repeat
export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
const {
actionErrorMessage,
aiDefaultsSummary,
actionNoticeMessage,
agents,
agentsError,
Expand All @@ -82,6 +85,7 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
startingAgentPubkey,
startingPersonaIds,
onBulkStopRunning,
onEditAiDefaults,
onOpenAgentProfile,
onOpenPersonaProfile,
onStartAgent,
Expand Down Expand Up @@ -160,11 +164,13 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {

<AgentsListHeader
agentCount={agents.length}
aiDefaultsSummary={aiDefaultsSummary}
fileInputRef={fileInputRef}
handleFileChange={handleFileChange}
isActionPending={isActionPending}
runningCount={runningCount}
onBulkStopRunning={onBulkStopRunning}
onEditAiDefaults={onEditAiDefaults}
/>

{isLoading ? <LoadingSkeleton /> : null}
Expand Down Expand Up @@ -437,18 +443,22 @@ function firstAvatarUrl(

function AgentsListHeader({
agentCount,
aiDefaultsSummary,
fileInputRef,
handleFileChange,
isActionPending,
runningCount,
onBulkStopRunning,
onEditAiDefaults,
}: {
agentCount: number;
aiDefaultsSummary: string;
fileInputRef: React.RefObject<HTMLInputElement | null>;
handleFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
isActionPending: boolean;
runningCount: number;
onBulkStopRunning: () => void;
onEditAiDefaults: () => void;
}) {
return (
<div className={AGENT_CARD_COLUMN_CLASS}>
Expand All @@ -461,7 +471,22 @@ function AgentsListHeader({
/>
<SectionHeader
title="Agents"
description="Agents in this community."
description={
<>
<span>Agents in this community.</span>
<span className="mt-1 flex flex-wrap items-center gap-x-1 text-xs">
<span>AI defaults: {aiDefaultsSummary}</span>
<Button
onClick={onEditAiDefaults}
size="xs"
type="button"
variant="link"
>
Edit defaults
</Button>
</span>
</>
}
action={
agentCount > 0 ? (
<DropdownMenu modal={false}>
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/agents/ui/bakedEnvHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export function getAdvancedInheritedSummary(
...(effort.value ? [`effort ${effort.value}`] : []),
...(globalEnvLabel ? [globalEnvLabel] : []),
];
return `Using global defaults: ${parts.join(" · ")}`;
return `Using AI defaults: ${parts.join(" · ")}`;
}

export function getInheritedAgentDefaults(
Expand Down
22 changes: 11 additions & 11 deletions desktop/src/features/agents/ui/createAgentLocalModeGate.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -840,8 +840,8 @@ test("providerDefaultLabel_globalSet_returnsInheritLabel", () => {
const label = getDefaultLlmProviderLabel("buzz-agent", "anthropic");
assert.equal(
label,
"Inherit global default (anthropic)",
"global provider set must return 'Inherit global default (<provider>)'",
"Use AI defaults (anthropic)",
"global provider set must return 'Use AI defaults (<provider>)'",
);
});

Expand All @@ -850,15 +850,15 @@ test("providerDefaultLabel_globalSetWithWhitespace_trimsAndReturnsInherit", () =
const label = getDefaultLlmProviderLabel("buzz-agent", " openai ");
assert.equal(
label,
"Inherit global default (openai)",
"Use AI defaults (openai)",
"global provider with surrounding whitespace must be trimmed in label",
);
});

test("providerDefaultLabel_sharedCompute_neverLeaksInternalId", () => {
assert.equal(
getDefaultLlmProviderLabel("buzz-agent", "relay-mesh"),
"Inherit global default (Buzz shared compute)",
"Use AI defaults (Buzz shared compute)",
);
});

Expand Down Expand Up @@ -972,8 +972,8 @@ test("modelDefaultLabel_globalSet_returnsInheritLabel", () => {
const label = getDefaultLlmModelLabel("claude-opus-4-5");
assert.equal(
label,
"Inherit global default (claude-opus-4-5)",
"global model set must return 'Inherit global default (<model>)'",
"Use AI defaults (claude-opus-4-5)",
"global model set must return 'Use AI defaults (<model>)'",
);
});

Expand All @@ -982,7 +982,7 @@ test("modelDefaultLabel_globalSetWithWhitespace_trimsAndReturnsInherit", () => {
const label = getDefaultLlmModelLabel(" gpt-4o ");
assert.equal(
label,
"Inherit global default (gpt-4o)",
"Use AI defaults (gpt-4o)",
"global model with surrounding whitespace must be trimmed in label",
);
});
Expand Down Expand Up @@ -1131,11 +1131,11 @@ test("f3_templateDialog_localProviderBlankGlobalAnthropicNoModel_saveBlocked", (

test("f3_templateDialog_globalModelSet_zeroValueLabelIsInherit", () => {
// Case 3: global model set → the zero-value model dropdown option must show
// "Inherit global default (<model>)" not the generic "Default model".
// "Use AI defaults (<model>)" not the generic "Default model".
// getDefaultLlmModelLabel is what AgentDefinitionDialog now uses for that slot.
assert.equal(
getDefaultLlmModelLabel("claude-opus-4-5"),
"Inherit global default (claude-opus-4-5)",
"Use AI defaults (claude-opus-4-5)",
"zero-value model option label must show the global model name when set",
);
assert.equal(
Expand Down Expand Up @@ -1175,7 +1175,7 @@ test("f3b_buildTemplateModelDropdownOptions_anthropicGlobalModelSet_containsInhe
);
assert.equal(
inheritEntry.label,
"Inherit global default (claude-opus-4-5)",
"Use AI defaults (claude-opus-4-5)",
"inherit entry must carry the global model name",
);
});
Expand Down Expand Up @@ -1215,7 +1215,7 @@ test("f3b_buildTemplateModelDropdownOptions_blankProviderGlobalModelSet_noDouble
);
assert.equal(
autoEntries[0].label,
"Inherit global default (claude-opus-4-5)",
"Use AI defaults (claude-opus-4-5)",
"existing zero-value entry must be relabeled with the global model name",
);
});
Expand Down
Loading