diff --git a/desktop/src/features/agents/lib/personaCatalogVisibility.test.mjs b/desktop/src/features/agents/lib/personaCatalogVisibility.test.mjs index 5259271e0..cf61c0c4c 100644 --- a/desktop/src/features/agents/lib/personaCatalogVisibility.test.mjs +++ b/desktop/src/features/agents/lib/personaCatalogVisibility.test.mjs @@ -2,7 +2,9 @@ import assert from "node:assert/strict"; import test from "node:test"; import { + readPublishedCatalogPersonaVersions, readSharedCatalogPersonaIds, + writePublishedCatalogPersonaVersions, writeSharedCatalogPersonaIds, } from "./personaCatalogVisibility.ts"; @@ -54,3 +56,49 @@ test("catalog visibility persists persona ids without blocking on storage errors }), ); }); + +test("catalog publication versions read only string revisions", () => { + const storage = { + getItem: () => + JSON.stringify({ + "custom:analyst": "2026-07-22T00:00:00.000Z", + "custom:invalid": 42, + }), + }; + + assert.deepEqual(readPublishedCatalogPersonaVersions(storage), { + "custom:analyst": "2026-07-22T00:00:00.000Z", + }); + assert.deepEqual(readPublishedCatalogPersonaVersions(null), {}); + assert.deepEqual( + readPublishedCatalogPersonaVersions({ getItem: () => "[]" }), + {}, + ); +}); + +test("catalog publication versions persist without blocking on storage errors", () => { + let storedKey = ""; + let storedValue = ""; + writePublishedCatalogPersonaVersions( + { "custom:analyst": "2026-07-22T00:00:00.000Z" }, + { + setItem: (key, value) => { + storedKey = key; + storedValue = value; + }, + }, + ); + + assert.equal(storedKey, "buzz-persona-catalog-published-versions-v1"); + assert.equal(storedValue, '{"custom:analyst":"2026-07-22T00:00:00.000Z"}'); + assert.doesNotThrow(() => + writePublishedCatalogPersonaVersions( + { "custom:analyst": "2026-07-22T00:00:00.000Z" }, + { + setItem: () => { + throw new Error("unavailable"); + }, + }, + ), + ); +}); diff --git a/desktop/src/features/agents/lib/personaCatalogVisibility.ts b/desktop/src/features/agents/lib/personaCatalogVisibility.ts index 43b30036e..433df35c3 100644 --- a/desktop/src/features/agents/lib/personaCatalogVisibility.ts +++ b/desktop/src/features/agents/lib/personaCatalogVisibility.ts @@ -1,5 +1,9 @@ const PERSONA_CATALOG_VISIBILITY_STORAGE_KEY = "buzz-persona-catalog-visibility-v1"; +const PERSONA_CATALOG_PUBLISHED_VERSIONS_STORAGE_KEY = + "buzz-persona-catalog-published-versions-v1"; + +export type PublishedCatalogPersonaVersions = Record; function resolveStorage( storage: Pick | null | undefined, @@ -49,3 +53,47 @@ export function writeSharedCatalogPersonaIds( // Catalog visibility is a convenience setting and should not block sharing. } } + +export function readPublishedCatalogPersonaVersions( + storage?: Pick | null, +): PublishedCatalogPersonaVersions { + const targetStorage = resolveStorage(storage); + if (!targetStorage) return {}; + + try { + const raw = targetStorage.getItem( + PERSONA_CATALOG_PUBLISHED_VERSIONS_STORAGE_KEY, + ); + if (!raw) return {}; + + const parsed: unknown = JSON.parse(raw); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return {}; + } + + return Object.fromEntries( + Object.entries(parsed).filter( + (entry): entry is [string, string] => typeof entry[1] === "string", + ), + ); + } catch { + return {}; + } +} + +export function writePublishedCatalogPersonaVersions( + versions: Readonly, + storage?: Pick | null, +): void { + const targetStorage = resolveStorage(storage); + if (!targetStorage) return; + + try { + targetStorage.setItem( + PERSONA_CATALOG_PUBLISHED_VERSIONS_STORAGE_KEY, + JSON.stringify(versions), + ); + } catch { + // Catalog publication state should not block sharing. + } +} diff --git a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx index 049fa8230..81bc3948a 100644 --- a/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx +++ b/desktop/src/features/agents/ui/AgentDefinitionDialog.tsx @@ -35,9 +35,9 @@ import { AUTO_MODEL_DROPDOWN_VALUE, AUTO_PROVIDER_DROPDOWN_VALUE, BLOCK_BUILD_HIDDEN_PROVIDER_IDS, + buildPersonaRuntimeDropdownOptions, CUSTOM_PROVIDER_DROPDOWN_VALUE, computeLocalModeGate, - formatRuntimeOptionLabel, getDefaultPersonaRuntime, getPersonaModelOptions, getPersonaProviderOptions, @@ -49,7 +49,6 @@ import { PERSONA_FIELD_SHELL_CLASS, PERSONA_LABEL_OPTIONAL_CLASS, shouldClearKnownModelForSelectionScope, - sortPersonaRuntimes, } from "./agentConfigOptions"; import { RequiredFieldLabel } from "./agentConfigControls"; import { @@ -97,13 +96,20 @@ type AgentDefinitionDialogProps = { onOpenChange: (open: boolean) => void; onSubmit: ( input: CreatePersonaInput | UpdatePersonaInput, + options: AgentDefinitionSubmitOptions, ) => Promise; + /** Offers publishing alongside Save after a shared agent has been edited. */ + showPublishUpdatesOption?: boolean; /** Rendered below the form fields in create mode only ("Where to run"). */ createRunSection?: React.ReactNode; /** Extra create-mode submit gate (e.g. incomplete provider config). */ createSubmitBlocked?: boolean; }; +export type AgentDefinitionSubmitOptions = { + publishCatalogUpdates: boolean; +}; + const ADVANCED_FIELDS_MOTION_TRANSITION = { duration: 0.18, ease: [0.23, 1, 0.32, 1], @@ -121,6 +127,7 @@ export function AgentDefinitionDialog({ runtimesLoading = false, onOpenChange, onSubmit, + showPublishUpdatesOption = false, createRunSection, createSubmitBlocked = false, }: AgentDefinitionDialogProps) { @@ -158,6 +165,9 @@ export function AgentDefinitionDialog({ const [showAdvancedFields, setShowAdvancedFields] = React.useState(false); const [isAvatarUploadPending, setIsAvatarUploadPending] = React.useState(false); + const [hasUserChanges, setHasUserChanges] = React.useState(false); + const [publishUpdatesChecked, setPublishUpdatesChecked] = + React.useState(false); const { globalConfig, inheritedDefaults: { @@ -213,6 +223,8 @@ export function AgentDefinitionDialog({ // sufficient reason to auto-open. setShowAdvancedFields(false); setIsAvatarUploadPending(false); + setHasUserChanges(false); + setPublishUpdatesChecked(false); isRuntimeAutoSeededRef.current = false; hasSeededForOpenRef.current = false; }, [initialValues, open]); @@ -258,6 +270,8 @@ export function AgentDefinitionDialog({ behaviorSeedRef.current = emptyPersonaBehaviorDraft; setShowAdvancedFields(false); setIsAvatarUploadPending(false); + setHasUserChanges(false); + setPublishUpdatesChecked(false); // isRuntimeAutoSeededRef and hasSeededForOpenRef are NOT reset here — the // [initialValues, open] effect resets both when the dialog re-opens. } @@ -309,14 +323,20 @@ export function AgentDefinitionDialog({ }; if ("id" in initialValues) { - await onSubmit({ - id: initialValues.id, - ...baseInput, - }); + await onSubmit( + { + id: initialValues.id, + ...baseInput, + }, + { + publishCatalogUpdates: + showPublishUpdatesOption && hasUserChanges && publishUpdatesChecked, + }, + ); return; } - await onSubmit(baseInput); + await onSubmit(baseInput, { publishCatalogUpdates: false }); } function handleSubmitForm(event: React.FormEvent) { @@ -342,6 +362,7 @@ export function AgentDefinitionDialog({ const { data: runtimeFileConfig, isLoading: fileConfigLoading } = useRuntimeFileConfigQuery(runtime, { enabled: open }); function handleAiConfigurationModeChange(nextMode: AgentAiConfigurationMode) { + setHasUserChanges(true); setAiConfigurationMode(nextMode); setIsCustomProviderEditing(false); setIsCustomModelEditing(false); @@ -552,41 +573,14 @@ export function AgentDefinitionDialog({ const showCustomProviderInput = llmProviderFieldVisible && isCustomProviderEditing; const runtimeDropdownValue = runtime.trim() || NO_RUNTIME_DROPDOWN_VALUE; - const sortedRuntimes = React.useMemo( - () => sortPersonaRuntimes(runtimes), - [runtimes], - ); - const blankRuntimeOptionLabel = runtimesLoading - ? "Loading harnesses..." - : isCreateMode - ? "Choose a harness" - : "No preference (use app default)"; - const runtimeDropdownOptions: PersonaDropdownOption[] = [ - ...(!isCreateMode - ? [ - { - label: blankRuntimeOptionLabel, - value: NO_RUNTIME_DROPDOWN_VALUE, - }, - ] - : []), - ...sortedRuntimes.map((candidate) => ({ - disabled: isCreateMode && candidate.availability !== "available", - label: `${formatRuntimeOptionLabel(candidate)}${ - isCreateMode && candidate.id === defaultRuntime?.id ? " (default)" : "" - }`, - value: candidate.id, - })), - ]; - if ( - runtime.trim().length > 0 && - !runtimeDropdownOptions.some((option) => option.value === runtime) - ) { - runtimeDropdownOptions.push({ - label: `${runtime.trim()} (current)`, - value: runtime.trim(), + const { blankRuntimeOptionLabel, runtimeDropdownOptions } = + buildPersonaRuntimeDropdownOptions({ + defaultRuntimeId: defaultRuntime?.id, + isCreateMode, + runtime, + runtimes, + runtimesLoading, }); - } const providerDropdownOptions: PersonaDropdownOption[] = [ ...providerOptions .filter((option) => option.id.trim().length > 0) @@ -673,6 +667,7 @@ export function AgentDefinitionDialog({ } function handleRuntimeDropdownChange(nextValue: string) { + setHasUserChanges(true); const nextRuntime = nextValue === NO_RUNTIME_DROPDOWN_VALUE ? "" : nextValue; // The user made an explicit choice — no longer auto-seeded. @@ -691,6 +686,7 @@ export function AgentDefinitionDialog({ } function handleProviderDropdownChange(nextValue: string) { + setHasUserChanges(true); const nextProvider = nextValue === AUTO_PROVIDER_DROPDOWN_VALUE ? "" : nextValue; if (nextProvider === "relay-mesh" && runtime !== "buzz-agent") { @@ -708,6 +704,7 @@ export function AgentDefinitionDialog({ } function handleModelDropdownChange(nextValue: string) { + setHasUserChanges(true); applySelection( selectionOnModelDropdownChange(selection, { nextValue, @@ -739,6 +736,9 @@ export function AgentDefinitionDialog({ isAvatarUploadPending={isAvatarUploadPending} isPending={isPending} onCancel={() => handleOpenChange(false)} + onPublishUpdatesCheckedChange={setPublishUpdatesChecked} + publishUpdatesChecked={publishUpdatesChecked} + showPublishUpdates={showPublishUpdatesOption && hasUserChanges} submitBlockReason={displayName.trim() ? submitBlockReason : null} submitLabel={submitLabel} /> @@ -747,15 +747,22 @@ export function AgentDefinitionDialog({
setHasUserChanges(true)} onSubmit={handleSubmitForm} > setAvatarUrl("")} + onClearAvatar={() => { + setHasUserChanges(true); + setAvatarUrl(""); + }} onUploadPendingChange={setIsAvatarUploadPending} - onSelectAvatar={setAvatarUrl} + onSelectAvatar={(nextAvatarUrl) => { + setHasUserChanges(true); + setAvatarUrl(nextAvatarUrl); + }} />
@@ -985,7 +992,10 @@ export function AgentDefinitionDialog({ model={model} modelTuningRuntimeId={runtime} namePoolText={namePoolText} - onBehaviorDraftChange={setBehaviorDraft} + onBehaviorDraftChange={(nextBehaviorDraft) => { + setHasUserChanges(true); + setBehaviorDraft(nextBehaviorDraft); + }} onEnvVarsChange={setEnvVars} onNamePoolTextChange={setNamePoolText} provider={effectiveProvider} diff --git a/desktop/src/features/agents/ui/AgentDefinitionDialogFooter.tsx b/desktop/src/features/agents/ui/AgentDefinitionDialogFooter.tsx index b7d6add9e..7d847bdc0 100644 --- a/desktop/src/features/agents/ui/AgentDefinitionDialogFooter.tsx +++ b/desktop/src/features/agents/ui/AgentDefinitionDialogFooter.tsx @@ -1,10 +1,14 @@ import { Button } from "@/shared/ui/button"; +import { Checkbox } from "@/shared/ui/checkbox"; type AgentDefinitionDialogFooterProps = { canSubmit: boolean; isAvatarUploadPending: boolean; isPending: boolean; onCancel: () => void; + onPublishUpdatesCheckedChange: (checked: boolean) => void; + publishUpdatesChecked: boolean; + showPublishUpdates: boolean; submitBlockReason: string | null; submitLabel: string; }; @@ -14,6 +18,9 @@ export function AgentDefinitionDialogFooter({ isAvatarUploadPending, isPending, onCancel, + onPublishUpdatesCheckedChange, + publishUpdatesChecked, + showPublishUpdates, submitBlockReason, submitLabel, }: AgentDefinitionDialogFooterProps) { @@ -51,6 +58,23 @@ export function AgentDefinitionDialogFooter({ ? "Uploading..." : submitLabel} + {showPublishUpdates ? ( + + ) : null}
); diff --git a/desktop/src/features/agents/ui/AgentDialog.tsx b/desktop/src/features/agents/ui/AgentDialog.tsx index 02a6d0e64..cf5deaed1 100644 --- a/desktop/src/features/agents/ui/AgentDialog.tsx +++ b/desktop/src/features/agents/ui/AgentDialog.tsx @@ -11,7 +11,10 @@ import type { AgentCreateIntent } from "./agentCreateIntent"; import type { EditAgentFocusTarget } from "@/features/agents/openEditAgentEvent"; import { AgentInstanceEditDialog } from "./AgentInstanceEditDialog"; import { createPersonaDialogState } from "./personaDialogState"; -import { AgentDefinitionDialog } from "./AgentDefinitionDialog"; +import { + AgentDefinitionDialog, + type AgentDefinitionSubmitOptions, +} from "./AgentDefinitionDialog"; import { WhereToRunSection } from "./WhereToRunSection"; import { canSubmitWhereToRun, @@ -64,7 +67,9 @@ type AgentDialogDefinitionEditProps = { onOpenChange: (open: boolean) => void; onSubmit: ( input: CreatePersonaInput | UpdatePersonaInput, + options: AgentDefinitionSubmitOptions, ) => Promise; + showPublishUpdatesOption?: boolean; }; type AgentDialogProps = diff --git a/desktop/src/features/agents/ui/AgentsView.tsx b/desktop/src/features/agents/ui/AgentsView.tsx index 352566f1a..60f8a7f12 100644 --- a/desktop/src/features/agents/ui/AgentsView.tsx +++ b/desktop/src/features/agents/ui/AgentsView.tsx @@ -322,8 +322,22 @@ export function AgentsView() { personas.setPersonaDialogState(null); } }} - onSubmit={personas.handleSubmit} + onSubmit={(input, options) => + personas.handleSubmit( + input, + undefined, + undefined, + undefined, + options, + ) + } open={personas.personaDialogState !== null} + showPublishUpdatesOption={ + "id" in personas.personaDialogState.initialValues && + personas.sharedCatalogPersonaIdSet.has( + personas.personaDialogState.initialValues.id, + ) + } submitLabel={personas.personaDialogState.submitLabel} title={personas.personaDialogState.title} /> @@ -349,6 +363,9 @@ export function AgentsView() { ) : null} {personas.personaToShare ? ( { + const shareTarget = personas.personaToShare; + if (!shareTarget) return; + personas.publishPersonaCatalogUpdates(shareTarget.persona); + }} open={personas.personaToShare !== null} persona={personas.personaToShare.persona} /> diff --git a/desktop/src/features/agents/ui/PersonaShareDialog.tsx b/desktop/src/features/agents/ui/PersonaShareDialog.tsx index 8f34ef196..4b5bb714d 100644 --- a/desktop/src/features/agents/ui/PersonaShareDialog.tsx +++ b/desktop/src/features/agents/ui/PersonaShareDialog.tsx @@ -53,6 +53,7 @@ import { resolveSnapshotAvatarPng } from "./snapshotAvatarPng"; import { useSnapshotSendController } from "./useSnapshotSendController"; type PersonaShareDialogProps = { + hasCatalogUpdates: boolean; isCatalogVisible: boolean; isPending: boolean; linkedAgentPubkey: string | null; @@ -60,6 +61,7 @@ type PersonaShareDialogProps = { onExport: () => void; onOpenChange: (open: boolean) => void; open: boolean; + onPublishCatalogUpdates: () => void; persona: AgentPersona; }; @@ -722,12 +724,14 @@ export function SnapshotShareDialog({ } export function PersonaShareDialog({ + hasCatalogUpdates, isCatalogVisible, isPending, linkedAgentPubkey, onCatalogVisibilityChange, onExport, onOpenChange, + onPublishCatalogUpdates, open, persona, }: PersonaShareDialogProps) { @@ -767,13 +771,26 @@ export function PersonaShareDialog({ Let anyone on this relay find and use this agent.

- +
+ + {isCatalogVisible && hasCatalogUpdates ? ( + + ) : null} +
) } diff --git a/desktop/src/features/agents/ui/agentConfigOptions.tsx b/desktop/src/features/agents/ui/agentConfigOptions.tsx index 6ae81ff6c..dd1120806 100644 --- a/desktop/src/features/agents/ui/agentConfigOptions.tsx +++ b/desktop/src/features/agents/ui/agentConfigOptions.tsx @@ -426,6 +426,57 @@ export function formatRuntimeOptionLabel(runtime: AcpRuntimeCatalogEntry) { return `${runtime.label}${suffix}`; } +export function buildPersonaRuntimeDropdownOptions({ + defaultRuntimeId, + isCreateMode, + runtime, + runtimes, + runtimesLoading, +}: { + defaultRuntimeId?: string; + isCreateMode: boolean; + runtime: string; + runtimes: AcpRuntimeCatalogEntry[]; + runtimesLoading: boolean; +}): { + blankRuntimeOptionLabel: string; + runtimeDropdownOptions: PersonaDropdownOption[]; +} { + const blankRuntimeOptionLabel = runtimesLoading + ? "Loading harnesses..." + : isCreateMode + ? "Choose a harness" + : "No preference (use app default)"; + const runtimeDropdownOptions: PersonaDropdownOption[] = [ + ...(!isCreateMode + ? [ + { + label: blankRuntimeOptionLabel, + value: NO_RUNTIME_DROPDOWN_VALUE, + }, + ] + : []), + ...sortPersonaRuntimes(runtimes).map((candidate) => ({ + disabled: isCreateMode && candidate.availability !== "available", + label: `${formatRuntimeOptionLabel(candidate)}${ + isCreateMode && candidate.id === defaultRuntimeId ? " (default)" : "" + }`, + value: candidate.id, + })), + ]; + const currentRuntime = runtime.trim(); + if ( + currentRuntime.length > 0 && + !runtimeDropdownOptions.some((option) => option.value === currentRuntime) + ) { + runtimeDropdownOptions.push({ + label: `${currentRuntime} (current)`, + value: currentRuntime, + }); + } + return { blankRuntimeOptionLabel, runtimeDropdownOptions }; +} + function runtimeAvailabilitySortRank( availability: AcpRuntimeCatalogEntry["availability"], ) { diff --git a/desktop/src/features/agents/ui/usePersonaActions.ts b/desktop/src/features/agents/ui/usePersonaActions.ts index 494b4ae1c..217a96a40 100644 --- a/desktop/src/features/agents/ui/usePersonaActions.ts +++ b/desktop/src/features/agents/ui/usePersonaActions.ts @@ -19,7 +19,9 @@ import { } from "@/features/agents/hooks"; import { getPersonaLibraryState } from "@/features/agents/lib/catalog"; import { + readPublishedCatalogPersonaVersions, readSharedCatalogPersonaIds, + writePublishedCatalogPersonaVersions, writeSharedCatalogPersonaIds, } from "@/features/agents/lib/personaCatalogVisibility"; import { useCreatedAgentChannelAttachment } from "@/features/agents/useCreatedAgentChannelAttachment"; @@ -94,6 +96,8 @@ export function usePersonaActions() { const [sharedCatalogPersonaIds, setSharedCatalogPersonaIds] = React.useState< string[] >(readSharedCatalogPersonaIds); + const [publishedCatalogPersonaVersions, setPublishedCatalogPersonaVersions] = + React.useState>(readPublishedCatalogPersonaVersions); const [personaNoticeMessage, setPersonaNoticeMessage] = React.useState< string | null >(null); @@ -137,6 +141,7 @@ export function usePersonaActions() { intent?: AgentCreateIntent, backendIntent?: BackendIntent | null, targetChannel?: Pick | null, + options?: { publishCatalogUpdates?: boolean }, ): Promise { if (isPersonaSubmitPending) { return false; @@ -146,7 +151,10 @@ export function usePersonaActions() { setIsPersonaSubmitPending(true); try { if ("id" in input) { - await updatePersonaMutation.mutateAsync(input); + const updatedPersona = await updatePersonaMutation.mutateAsync(input); + if (options?.publishCatalogUpdates) { + publishPersonaCatalogUpdates(updatedPersona); + } setPersonaNoticeMessage(`Updated ${input.displayName}.`); } else { const runtime = availableRuntimes.find( @@ -354,6 +362,16 @@ export function usePersonaActions() { linkedAgent: ManagedAgent | undefined, ) { clearFeedback("library"); + if ( + sharedCatalogPersonaIdSet.has(persona.id) && + publishedCatalogPersonaVersions[persona.id] === undefined + ) { + setPublishedCatalogPersonaVersions((current) => { + const next = { ...current, [persona.id]: persona.updatedAt }; + writePublishedCatalogPersonaVersions(next); + return next; + }); + } setPersonaToShare({ persona, linkedAgentPubkey: linkedAgent?.pubkey ?? null, @@ -412,6 +430,35 @@ export function usePersonaActions() { writeSharedCatalogPersonaIds(ids); return ids; }); + setPublishedCatalogPersonaVersions((current) => { + const next = { ...current }; + if (visible) { + next[persona.id] = persona.updatedAt; + } else { + delete next[persona.id]; + } + writePublishedCatalogPersonaVersions(next); + return next; + }); + } + + function hasPersonaCatalogUpdates(persona: AgentPersona) { + const publishedVersion = publishedCatalogPersonaVersions[persona.id]; + return ( + sharedCatalogPersonaIdSet.has(persona.id) && + publishedVersion !== undefined && + publishedVersion !== persona.updatedAt + ); + } + + function publishPersonaCatalogUpdates(persona: AgentPersona) { + if (persona.isBuiltIn || !sharedCatalogPersonaIdSet.has(persona.id)) return; + + setPublishedCatalogPersonaVersions((current) => { + const next = { ...current, [persona.id]: persona.updatedAt }; + writePublishedCatalogPersonaVersions(next); + return next; + }); } const isPending = @@ -460,6 +507,8 @@ export function usePersonaActions() { setPersonaToExportSnapshot, handleExportSnapshot, setPersonaCatalogVisibility, + hasPersonaCatalogUpdates, + publishPersonaCatalogUpdates, sharedCatalogPersonaIdSet, clearFeedback, snapshotImportState, diff --git a/desktop/tests/e2e/agents.spec.ts b/desktop/tests/e2e/agents.spec.ts index df7713266..b822013db 100644 --- a/desktop/tests/e2e/agents.spec.ts +++ b/desktop/tests/e2e/agents.spec.ts @@ -1288,6 +1288,11 @@ test("custom personas share with people and keep export separate", async ({ test("custom personas can be shared to the relay catalog", async ({ page }) => { const personaId = "custom:catalog-analyst"; await installMockBridge(page, { + globalAgentConfig: { + env_vars: { ANTHROPIC_API_KEY: "sk-ant-test" }, + provider: "anthropic", + model: "claude-opus-4-5", + }, personas: [ { id: personaId, @@ -1319,9 +1324,14 @@ This deliberately long fenced-code example must not establish the minimum width await page.getByLabel("Open actions for Catalog Analyst").click(); await page.getByRole("menuitem", { name: "Share" }).click(); const catalogToggle = page.getByTestId("persona-share-show-in-catalog"); + const publishCatalogUpdatesButton = page.getByTestId( + "persona-share-publish-catalog-updates", + ); await expect(catalogToggle).toHaveAttribute("aria-checked", "false"); + await expect(publishCatalogUpdatesButton).toHaveCount(0); await catalogToggle.click(); await expect(catalogToggle).toHaveAttribute("aria-checked", "true"); + await expect(publishCatalogUpdatesButton).toHaveCount(0); await page .getByTestId("persona-share-dialog") .getByRole("button", { name: "Close" }) @@ -1347,9 +1357,65 @@ This deliberately long fenced-code example must not establish the minimum width ).toBeLessThanOrEqual(1); await page.keyboard.press("Escape"); + await page.getByLabel("Open actions for Catalog Analyst").click(); + await page.getByRole("menuitem", { name: "Edit" }).click(); + const editDialog = page.getByTestId("persona-dialog"); + const publishUpdatesCheckbox = editDialog.getByTestId( + "persona-dialog-publish-updates", + ); + await expect(publishUpdatesCheckbox).toHaveCount(0); + await editDialog + .getByLabel("Agent instructions") + .fill("Review the latest catalog changes."); + await expect(publishUpdatesCheckbox).toBeVisible(); + await expect(publishUpdatesCheckbox).toHaveAttribute( + "data-state", + "unchecked", + ); + const [saveButtonBox, publishUpdatesCheckboxBox] = await Promise.all([ + editDialog.getByRole("button", { name: "Save changes" }).boundingBox(), + publishUpdatesCheckbox.boundingBox(), + ]); + expect(publishUpdatesCheckboxBox?.x ?? 0).toBeGreaterThan( + (saveButtonBox?.x ?? 0) + (saveButtonBox?.width ?? 0), + ); + await editDialog.getByRole("button", { name: "Save changes" }).click(); + await expect(editDialog).toHaveCount(0); + await page.getByLabel("Open actions for Catalog Analyst").click(); await page.getByRole("menuitem", { name: "Share" }).click(); await expect(catalogToggle).toHaveAttribute("aria-checked", "true"); + await expect(publishCatalogUpdatesButton).toBeVisible(); + const [catalogToggleBox, publishCatalogUpdatesButtonBox] = await Promise.all([ + catalogToggle.boundingBox(), + publishCatalogUpdatesButton.boundingBox(), + ]); + expect(publishCatalogUpdatesButtonBox?.x ?? 0).toBeGreaterThan( + (catalogToggleBox?.x ?? 0) + (catalogToggleBox?.width ?? 0), + ); + await publishCatalogUpdatesButton.click(); + await expect(publishCatalogUpdatesButton).toHaveCount(0); + await expect(catalogToggle).toHaveAttribute("aria-checked", "true"); + await page + .getByTestId("persona-share-dialog") + .getByRole("button", { name: "Close" }) + .click(); + + await page.getByLabel("Open actions for Catalog Analyst").click(); + await page.getByRole("menuitem", { name: "Edit" }).click(); + await editDialog + .getByLabel("Agent instructions") + .fill("Review and publish the latest catalog changes."); + await expect(publishUpdatesCheckbox).toBeVisible(); + await publishUpdatesCheckbox.click(); + await expect(publishUpdatesCheckbox).toHaveAttribute("data-state", "checked"); + await editDialog.getByRole("button", { name: "Save changes" }).click(); + await expect(editDialog).toHaveCount(0); + + await page.getByLabel("Open actions for Catalog Analyst").click(); + await page.getByRole("menuitem", { name: "Share" }).click(); + await expect(catalogToggle).toHaveAttribute("aria-checked", "true"); + await expect(publishCatalogUpdatesButton).toHaveCount(0); await catalogToggle.click(); await page .getByTestId("persona-share-dialog")