Add catalog update publishing

This commit is contained in:
kenny lopez
2026-07-23 08:36:57 -07:00
parent d41e6cc853
commit 1320d6426b
10 changed files with 394 additions and 54 deletions
@@ -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");
},
},
),
);
});
@@ -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<string, string>;
function resolveStorage<T extends "getItem" | "setItem">(
storage: Pick<Storage, T> | 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<Storage, "getItem"> | 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<PublishedCatalogPersonaVersions>,
storage?: Pick<Storage, "setItem"> | 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.
}
}
@@ -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<unknown>;
/** 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<HTMLFormElement>) {
@@ -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({
<form
className="grid gap-5 lg:grid-cols-[220px_minmax(0,1fr)]"
id="persona-dialog-form"
onChangeCapture={() => setHasUserChanges(true)}
onSubmit={handleSubmitForm}
>
<AgentCreationPreview
avatarUrl={previewAvatarUrl}
disabled={isPending || isAvatarUploadPending}
label={previewLabel}
onClearAvatar={() => setAvatarUrl("")}
onClearAvatar={() => {
setHasUserChanges(true);
setAvatarUrl("");
}}
onUploadPendingChange={setIsAvatarUploadPending}
onSelectAvatar={setAvatarUrl}
onSelectAvatar={(nextAvatarUrl) => {
setHasUserChanges(true);
setAvatarUrl(nextAvatarUrl);
}}
/>
<div className="space-y-5">
@@ -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}
@@ -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}
</Button>
{showPublishUpdates ? (
<label
className="ml-1 flex cursor-pointer items-center gap-2 text-sm font-medium text-foreground"
htmlFor="persona-dialog-publish-updates"
>
<Checkbox
checked={publishUpdatesChecked}
data-testid="persona-dialog-publish-updates"
disabled={isPending || isAvatarUploadPending}
id="persona-dialog-publish-updates"
onCheckedChange={(checked) =>
onPublishUpdatesCheckedChange(checked === true)
}
/>
<span>Publish updates</span>
</label>
) : null}
</div>
</div>
);
@@ -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<unknown>;
showPublishUpdatesOption?: boolean;
};
type AgentDialogProps =
+23 -1
View File
@@ -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 ? (
<PersonaShareDialog
hasCatalogUpdates={personas.hasPersonaCatalogUpdates(
personas.personaToShare.persona,
)}
isCatalogVisible={
personas.personaToShare.persona.isBuiltIn ||
personas.sharedCatalogPersonaIdSet.has(
@@ -373,6 +390,11 @@ export function AgentsView() {
personas.setPersonaToShare(null);
}
}}
onPublishCatalogUpdates={() => {
const shareTarget = personas.personaToShare;
if (!shareTarget) return;
personas.publishPersonaCatalogUpdates(shareTarget.persona);
}}
open={personas.personaToShare !== null}
persona={personas.personaToShare.persona}
/>
@@ -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.
</p>
</div>
<Switch
checked={isCatalogVisible}
data-testid="persona-share-show-in-catalog"
disabled={isPending}
id={switchId}
onCheckedChange={onCatalogVisibilityChange}
/>
<div className="flex shrink-0 items-center gap-2">
<Switch
checked={isCatalogVisible}
data-testid="persona-share-show-in-catalog"
disabled={isPending}
id={switchId}
onCheckedChange={onCatalogVisibilityChange}
/>
{isCatalogVisible && hasCatalogUpdates ? (
<Button
data-testid="persona-share-publish-catalog-updates"
disabled={isPending}
onClick={onPublishCatalogUpdates}
size="sm"
variant="outline"
>
Publish updates
</Button>
) : null}
</div>
</section>
)
}
@@ -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"],
) {
@@ -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<Record<string, string>>(readPublishedCatalogPersonaVersions);
const [personaNoticeMessage, setPersonaNoticeMessage] = React.useState<
string | null
>(null);
@@ -137,6 +141,7 @@ export function usePersonaActions() {
intent?: AgentCreateIntent,
backendIntent?: BackendIntent | null,
targetChannel?: Pick<Channel, "id" | "name"> | null,
options?: { publishCatalogUpdates?: boolean },
): Promise<boolean> {
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,
+66
View File
@@ -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")