mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Move agent AI defaults into Settings (#1919)
Signed-off-by: npub1d6t84ajeg9skp2609l2k6axgcme8x7g7u7luj352r03hcwreg7lqnxcsex <6e967af659416160ab4f2fd56d74c8c6f273791ee7bfc9468a1be37c387947be@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1d6t84ajeg9skp2609l2k6axgcme8x7g7u7luj352r03hcwreg7lqnxcsex <6e967af659416160ab4f2fd56d74c8c6f273791ee7bfc9468a1be37c387947be@sprout-oss.stage.blox.sqprod.co> Co-authored-by: Jordan Mecom <jm@squareup.com>
This commit is contained in:
co-authored by
npub1d6t84ajeg9skp2609l2k6axgcme8x7g7u7luj352r03hcwreg7lqnxcsex
Jordan Mecom
parent
335413461b
commit
3e90dd5dd8
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
getBakedProviderInheritLabel,
|
||||
} from "./bakedEnvHelpers";
|
||||
import { useAgentDialogDefaults } from "./useAgentDialogDefaults";
|
||||
import { AgentAiDefaultsNotice } from "./AgentAiDefaults";
|
||||
import { useProviderApiKeyFieldState } from "./providerApiKeyFieldState";
|
||||
import { buildRuntimeModelProviderPayload } from "./agentDefinitionSubmitPayload";
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 = {
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -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}
|
||||
|
||||
@@ -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[];
|
||||
@@ -41,6 +42,7 @@ type UnifiedAgentsSectionProps = {
|
||||
startingAgentPubkey: string | null;
|
||||
startingPersonaIds: ReadonlySet<string>;
|
||||
onBulkStopRunning: () => void;
|
||||
onEditAiDefaults: () => void;
|
||||
onOpenAgentProfile: (
|
||||
pubkey: string,
|
||||
options?: ProfilePanelOpenOptions,
|
||||
@@ -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,
|
||||
@@ -82,6 +85,7 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
|
||||
startingAgentPubkey,
|
||||
startingPersonaIds,
|
||||
onBulkStopRunning,
|
||||
onEditAiDefaults,
|
||||
onOpenAgentProfile,
|
||||
onOpenPersonaProfile,
|
||||
onStartAgent,
|
||||
@@ -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}
|
||||
@@ -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}>
|
||||
@@ -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}>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>)'",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -850,7 +850,7 @@ 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",
|
||||
);
|
||||
});
|
||||
@@ -858,7 +858,7 @@ test("providerDefaultLabel_globalSetWithWhitespace_trimsAndReturnsInherit", () =
|
||||
test("providerDefaultLabel_sharedCompute_neverLeaksInternalId", () => {
|
||||
assert.equal(
|
||||
getDefaultLlmProviderLabel("buzz-agent", "relay-mesh"),
|
||||
"Inherit global default (Buzz shared compute)",
|
||||
"Use AI defaults (Buzz shared compute)",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>)'",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
@@ -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(
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -271,21 +271,19 @@ export function getDefaultLlmProviderLabel(
|
||||
) {
|
||||
const trimmedGlobal = (globalProvider ?? "").trim();
|
||||
return trimmedGlobal
|
||||
? `Inherit global default (${providerDisplayLabel(trimmedGlobal)})`
|
||||
? `Use AI defaults (${providerDisplayLabel(trimmedGlobal)})`
|
||||
: "Select a provider\u2026";
|
||||
}
|
||||
|
||||
/** Returns the zero-value model option label.
|
||||
*
|
||||
* When a global model is configured, the empty-model option reads
|
||||
* `Inherit global default (<model>)` so users can see which model will run.
|
||||
* `Use AI defaults (<model>)` so users can see which model will run.
|
||||
* Otherwise falls back to the generic `"Default model"` placeholder.
|
||||
*/
|
||||
export function getDefaultLlmModelLabel(globalModel?: string) {
|
||||
const trimmedGlobal = (globalModel ?? "").trim();
|
||||
return trimmedGlobal
|
||||
? `Inherit global default (${trimmedGlobal})`
|
||||
: "Default model";
|
||||
return trimmedGlobal ? `Use AI defaults (${trimmedGlobal})` : "Default model";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,7 +292,7 @@ export function getDefaultLlmModelLabel(globalModel?: string) {
|
||||
*
|
||||
* Explicit-model providers (e.g. anthropic) have their zero-value option
|
||||
* filtered out by `getPersonaModelOptions`, so a relabel-only map would never
|
||||
* produce the `Inherit global default (<model>)` entry. This helper prepends
|
||||
* produce the `Use AI defaults (<model>)` entry. This helper prepends
|
||||
* it when `globalModel` is non-empty AND no zero-value option already exists,
|
||||
* making the inherited global model visible and selectable in the dropdown.
|
||||
*
|
||||
|
||||
@@ -118,12 +118,12 @@ test("resolveInheritedRuntimeSubmission preserves a user-edited provider + env w
|
||||
assert.equal(result.model, null);
|
||||
});
|
||||
|
||||
test("resolveInheritedRuntimeSubmission clears an already-inheriting agent's provider override when the user picks Default", () => {
|
||||
// Regression: an already-inheriting agent had a saved provider override
|
||||
// (databricks). The user picks the "Default" option → empty local provider.
|
||||
// Because the agent was NOT harness-pinned at open, this is a deliberate
|
||||
// clear, not the inherit-transition — persist null (runtime default), do NOT
|
||||
// resurrect the persona provider.
|
||||
test("resolveInheritedRuntimeSubmission clears an already-inheriting agent's persona-backed provider and model to AI defaults", () => {
|
||||
// Regression: an already-inheriting agent is linked to a persona with a
|
||||
// provider and model. The user picks "Use AI defaults" for both fields.
|
||||
// Because the agent was NOT harness-pinned at open, these empty local values
|
||||
// are deliberate clears, not an inherit-transition — persist null for both
|
||||
// rather than resurrecting the persona values.
|
||||
const result = resolveInheritedRuntimeSubmission({
|
||||
inheritHarness: true,
|
||||
agentWasHarnessPinned: false,
|
||||
@@ -135,6 +135,7 @@ test("resolveInheritedRuntimeSubmission clears an already-inheriting agent's pro
|
||||
personaEnvVars: { ANTHROPIC_API_KEY: "sk-persona" },
|
||||
});
|
||||
assert.equal(result.provider, null);
|
||||
assert.equal(result.model, null);
|
||||
assert.deepEqual(result.envVars, {});
|
||||
});
|
||||
|
||||
|
||||
@@ -138,8 +138,8 @@ export function GlobalAgentConfigSettingsCard() {
|
||||
data-testid="settings-global-agent-config"
|
||||
>
|
||||
<SectionHeader
|
||||
title="Agent defaults"
|
||||
description="Default settings for every agent running on this computer. You can override them for any individual agent or persona."
|
||||
title="AI defaults"
|
||||
description="Provider, model, effort, and environment settings inherited by local agents. Agent-specific settings always take priority."
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
|
||||
@@ -63,6 +63,7 @@ import { MobilePairingCard } from "./MobilePairingCard";
|
||||
import { ModerationQueueCard } from "./ModerationQueueCard";
|
||||
import { NotificationSettingsCard } from "./NotificationSettingsCard";
|
||||
import { PreventSleepSettingsCard } from "./PreventSleepSettingsCard";
|
||||
import { GlobalAgentConfigSettingsCard } from "./GlobalAgentConfigSettingsCard";
|
||||
import { ProfileSettingsCard } from "./ProfileSettingsCard";
|
||||
import { UpdateChecker } from "../UpdateChecker";
|
||||
import { SettingsSectionHeader } from "./SettingsSectionHeader";
|
||||
@@ -712,7 +713,12 @@ export function renderSettingsSection(
|
||||
case "experimental":
|
||||
return <ExperimentalFeaturesCard />;
|
||||
case "agents":
|
||||
return <PreventSleepSettingsCard />;
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
<GlobalAgentConfigSettingsCard />
|
||||
<PreventSleepSettingsCard />
|
||||
</div>
|
||||
);
|
||||
case "channel-templates":
|
||||
return <ChannelTemplatesSettingsCard />;
|
||||
case "compute":
|
||||
|
||||
@@ -26,16 +26,25 @@ const CASCADE_AGENT_A_PUBKEY = "aa".repeat(32);
|
||||
const CASCADE_AGENT_B_PUBKEY = "bb".repeat(32);
|
||||
|
||||
/**
|
||||
* Navigate to the Agents view and wait for the global agent config card to
|
||||
* finish loading (spinner gone). The card lives at the bottom of the view.
|
||||
* Navigate to the Agents view and wait for its unified list to mount.
|
||||
*/
|
||||
async function openAgentsView(page: import("@playwright/test").Page) {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("open-agents-view").click();
|
||||
await expect(page.getByTestId("unified-agents-groups")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
}
|
||||
|
||||
async function openAiDefaultsSettings(page: import("@playwright/test").Page) {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByTestId("open-settings").click();
|
||||
await page.getByTestId("profile-popover-settings").click();
|
||||
await expect(page.getByTestId("settings-view")).toBeVisible();
|
||||
await page.getByTestId("settings-nav-agents").click();
|
||||
await expect(page.getByTestId("settings-global-agent-config")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
// Spinner disappears once the load effect resolves.
|
||||
await expect(page.locator(".animate-spin").first()).not.toBeVisible({
|
||||
timeout: 5_000,
|
||||
});
|
||||
@@ -120,7 +129,7 @@ test.describe("agent lifecycle feedback screenshots", () => {
|
||||
globalConfigRestartedCount: 2,
|
||||
});
|
||||
|
||||
await openAgentsView(page);
|
||||
await openAiDefaultsSettings(page);
|
||||
|
||||
const card = page.getByTestId("settings-global-agent-config");
|
||||
|
||||
@@ -150,7 +159,7 @@ test.describe("agent lifecycle feedback screenshots", () => {
|
||||
test("03-save-plain", async ({ page }) => {
|
||||
await installMockBridge(page);
|
||||
|
||||
await openAgentsView(page);
|
||||
await openAiDefaultsSettings(page);
|
||||
|
||||
const card = page.getByTestId("settings-global-agent-config");
|
||||
|
||||
@@ -264,7 +273,7 @@ test.describe("agent lifecycle feedback screenshots", () => {
|
||||
globalConfigRestartedCount: 1,
|
||||
});
|
||||
|
||||
await openAgentsView(page);
|
||||
await openAiDefaultsSettings(page);
|
||||
|
||||
const card = page.getByTestId("settings-global-agent-config");
|
||||
|
||||
@@ -287,7 +296,7 @@ test.describe("agent lifecycle feedback screenshots", () => {
|
||||
globalConfigFailedRestartCount: 1,
|
||||
});
|
||||
|
||||
await openAgentsView(page);
|
||||
await openAiDefaultsSettings(page);
|
||||
|
||||
const card = page.getByTestId("settings-global-agent-config");
|
||||
|
||||
@@ -319,7 +328,7 @@ test.describe("agent lifecycle feedback screenshots", () => {
|
||||
globalConfigSaveDelayMs: 2_000,
|
||||
});
|
||||
|
||||
await openAgentsView(page);
|
||||
await openAiDefaultsSettings(page);
|
||||
|
||||
const card = page.getByTestId("settings-global-agent-config");
|
||||
const provider = page.locator("#global-agent-provider");
|
||||
|
||||
@@ -24,12 +24,16 @@ import { waitForAnimations } from "../helpers/animations";
|
||||
const SHOTS = "test-results/screenshots-dialogs";
|
||||
|
||||
/**
|
||||
* Navigate to the agents view and wait for the global agent config card to
|
||||
* finish its async load (spinner gone, card content visible).
|
||||
* Open Settings → Agents through the app UI and wait for the defaults card to
|
||||
* finish loading. The CI static server does not provide SPA fallbacks for a
|
||||
* direct `/settings` request.
|
||||
*/
|
||||
async function openAgentsView(page: import("@playwright/test").Page) {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("open-agents-view").click();
|
||||
async function openAiDefaultsSettings(page: import("@playwright/test").Page) {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByTestId("open-settings").click();
|
||||
await page.getByTestId("profile-popover-settings").click();
|
||||
await expect(page.getByTestId("settings-view")).toBeVisible();
|
||||
await page.getByTestId("settings-nav-agents").click();
|
||||
await expect(page.getByTestId("settings-global-agent-config")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
@@ -58,7 +62,7 @@ test.describe("agent provider dropdown screenshots", () => {
|
||||
// BUZZ_AGENT_PROVIDER is baked and hideProviderIds is empty → v1 appears.
|
||||
test("01-provider-dropdown-oss", async ({ page }) => {
|
||||
await installMockBridge(page);
|
||||
await openAgentsView(page);
|
||||
await openAiDefaultsSettings(page);
|
||||
|
||||
const providerSelect = page.locator("#global-agent-provider");
|
||||
await expect(providerSelect).toBeVisible({ timeout: 5_000 });
|
||||
@@ -98,7 +102,7 @@ test.describe("agent provider dropdown screenshots", () => {
|
||||
env_vars: {},
|
||||
},
|
||||
});
|
||||
await openAgentsView(page);
|
||||
await openAiDefaultsSettings(page);
|
||||
|
||||
const effortSelect = page.locator("#global-agent-thinking-effort");
|
||||
await expect(effortSelect).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
@@ -202,14 +202,12 @@ test.describe("edit agent dialog", () => {
|
||||
await openEditDialog(page);
|
||||
|
||||
await expect(page.locator("#edit-agent-llm-provider")).toHaveText(
|
||||
"Inherit global default (anthropic)",
|
||||
"Use AI defaults (anthropic)",
|
||||
);
|
||||
await expect(page.locator("#edit-agent-model")).toHaveText(
|
||||
"Inherit global default (claude-opus-4-5)",
|
||||
"Use AI defaults (claude-opus-4-5)",
|
||||
);
|
||||
await expect(
|
||||
page.getByText("Using global defaults: effort low"),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("Using AI defaults: effort low")).toBeVisible();
|
||||
});
|
||||
|
||||
test("profile Edit routes persona-linked agents to the definition editor", async ({
|
||||
|
||||
@@ -12,17 +12,19 @@ async function settleAnimations(page: import("@playwright/test").Page) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the Agents view (where GlobalAgentConfigSettingsCard lives) and
|
||||
* wait for the card to finish loading.
|
||||
* Open Settings → Agents through the app UI and wait for the defaults card to
|
||||
* load. CI serves the built SPA with a static file server, so navigating to
|
||||
* `/settings` directly returns a 404 before the client router can start.
|
||||
*/
|
||||
async function openAgentsView(page: import("@playwright/test").Page) {
|
||||
await page.goto("/");
|
||||
await page.getByTestId("open-agents-view").click();
|
||||
// Wait for the global agent config card to mount and finish its load effect.
|
||||
async function openAiDefaultsSettings(page: import("@playwright/test").Page) {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByTestId("open-settings").click();
|
||||
await page.getByTestId("profile-popover-settings").click();
|
||||
await expect(page.getByTestId("settings-view")).toBeVisible();
|
||||
await page.getByTestId("settings-nav-agents").click();
|
||||
await expect(page.getByTestId("settings-global-agent-config")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
// The card shows a spinner while loading; wait for it to disappear.
|
||||
await expect(page.locator(".animate-spin").first()).not.toBeVisible({
|
||||
timeout: 5_000,
|
||||
});
|
||||
@@ -65,7 +67,7 @@ test.describe("global agent config screenshots", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await openAgentsView(page);
|
||||
await openAiDefaultsSettings(page);
|
||||
|
||||
const card = page.getByTestId("settings-global-agent-config");
|
||||
await card.scrollIntoViewIfNeeded();
|
||||
@@ -187,14 +189,12 @@ test.describe("global agent config screenshots", () => {
|
||||
await openCreateDialog(page);
|
||||
|
||||
await expect(page.locator("#persona-llm-provider")).toHaveText(
|
||||
"Inherit global default (anthropic)",
|
||||
"Use AI defaults (anthropic)",
|
||||
);
|
||||
await expect(page.locator("#persona-model")).toHaveText(
|
||||
"Inherit global default (claude-opus-4-5)",
|
||||
"Use AI defaults (claude-opus-4-5)",
|
||||
);
|
||||
await expect(
|
||||
page.getByText("Using global defaults: effort low"),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("Using AI defaults: effort low")).toBeVisible();
|
||||
});
|
||||
|
||||
// Shot 04: Create gate BLOCKED — no per-agent provider, no global provider
|
||||
|
||||
Reference in New Issue
Block a user