From 4cba11457bb54e32f638c19bed6263dcf273e2b2 Mon Sep 17 00:00:00 2001 From: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz> Date: Wed, 29 Jul 2026 19:11:21 -0400 Subject: [PATCH 1/2] feat(catalog): resolve publisher display name in catalog detail pane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `PersonaCatalogDetail` component hardcoded the label passed to `PersonaAddedBy` as "Community member" for any non-own catalog entry. The publisher's pubkey is already available as `catalogSource.ownerPubkey` (set from the kind:30175 event's pubkey). Use `useUsersBatchQuery` to resolve the pubkey to a kind:0 display name. Label precedence: own entry → "You"; resolved profile → `displayName` then `name`; loading or unresolvable → "Community member" (unchanged fallback). The batch query is disabled when the entry is not a community entry, so there is no extra network call for own entries or built-in agents. E2e: update the existing assertion that was checking for the hardcoded fallback, add a new test that verifies "Community member" still renders when the publisher pubkey is not in the mock profile registry. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- .../agents/ui/PersonaCatalogDialog.tsx | 29 ++++++++++----- desktop/tests/e2e/agents.spec.ts | 36 ++++++++++++++++++- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/desktop/src/features/agents/ui/PersonaCatalogDialog.tsx b/desktop/src/features/agents/ui/PersonaCatalogDialog.tsx index ba76d6e4ed..f8549aca35 100644 --- a/desktop/src/features/agents/ui/PersonaCatalogDialog.tsx +++ b/desktop/src/features/agents/ui/PersonaCatalogDialog.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { isCatalogPersonaSelected } from "@/features/agents/lib/catalog"; import { isCatalogPersona } from "@/features/agents/lib/personaCatalogRelay"; +import { useUsersBatchQuery } from "@/features/profile/hooks"; import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar"; import type { AgentPersona } from "@/shared/api/types"; import { useFeedbackToasts } from "@/shared/hooks/useToastEffect"; @@ -277,6 +278,25 @@ function PersonaCatalogChooser({ } function PersonaCatalogDetail({ persona }: { persona: AgentPersona }) { + const isCommunityEntry = + isCatalogPersona(persona) && !persona.catalogSource.isOwn; + const ownerPubkey = isCommunityEntry + ? persona.catalogSource.ownerPubkey + : undefined; + const ownerBatchQuery = useUsersBatchQuery(ownerPubkey ? [ownerPubkey] : [], { + enabled: !!ownerPubkey, + }); + + let addedByLabel: string; + if (!isCommunityEntry) { + addedByLabel = "You"; + } else { + const summary = ownerPubkey + ? ownerBatchQuery.data?.profiles[ownerPubkey.toLowerCase()] + : undefined; + addedByLabel = summary?.displayName ?? summary?.name ?? "Community member"; + } + return (
@@ -290,14 +310,7 @@ function PersonaCatalogDetail({ persona }: { persona: AgentPersona }) { {persona.displayName} {persona.isBuiltIn ? null : ( - + )}
diff --git a/desktop/tests/e2e/agents.spec.ts b/desktop/tests/e2e/agents.spec.ts index 8c1c407df1..a86443f58a 100644 --- a/desktop/tests/e2e/agents.spec.ts +++ b/desktop/tests/e2e/agents.spec.ts @@ -1791,8 +1791,10 @@ test("a community member can discover and add another member's catalog agent", a ); await expect(remoteEntry).toContainText("Alice’s Reviewer"); await remoteEntry.click(); + // The detail pane resolves the publisher's display name; 'Community member' + // is only the fallback for an unresolvable pubkey. await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText( - "Added by Community member", + "Added by alice", ); await page @@ -1845,6 +1847,38 @@ test("a community member can discover and add another member's catalog agent", a expect(await countCommandInvocations(page, "create_persona")).toBe(1); }); +test("catalog detail shows Community member when the publisher profile cannot be resolved", async ({ + page, +}) => { + // A pubkey that is not in the mock profile registry — profile resolution + // will fail and the detail pane must fall back gracefully. + const unknownPubkey = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const personaId = "unresolvable-reviewer"; + await installMockBridge(page, { + personaCatalogEvents: [ + createCatalogEvent({ + ownerPubkey: unknownPubkey, + sourcePersonaId: personaId, + displayName: "Mystery Agent", + systemPrompt: "Published by someone whose profile cannot be fetched.", + }), + ], + }); + await gotoApp(page); + await page.getByTestId("open-agents-view").click(); + await openPersonaCatalog(page); + + await page + .getByTestId( + `persona-catalog-list-item-catalog:${unknownPubkey}:${personaId}`, + ) + .click(); + await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText( + "Added by Community member", + ); +}); + test("one share level selector drives both the link and send paths", async ({ page, }) => { From f1a03bde87e6eb680de6f9fef57ff3e45c1a738b Mon Sep 17 00:00:00 2001 From: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz> Date: Thu, 30 Jul 2026 11:08:24 -0400 Subject: [PATCH 2/2] fix(catalog): normalize owner label with truthy fallbacks, add unit tests Thufir (review at b5703187) found that a present-but-empty or whitespace-only kind:0 `displayName` is not nullish, so the `??` chain stopped early and the detail pane rendered 'Added by ' with a blank label. Replace `??` with truthy short-circuit on trimmed values, and extract the derivation into `resolveCatalogOwnerLabel` so it can be exercised directly. Unit tests cover: populated displayName wins; whitespace-only displayName falls through to name; both candidates empty/whitespace/null/undefined all fall through to 'Community member'. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- .../agents/ui/PersonaCatalogDialog.tsx | 18 ++++- .../ui/personaCatalogOwnerLabel.test.mjs | 77 +++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 desktop/src/features/agents/ui/personaCatalogOwnerLabel.test.mjs diff --git a/desktop/src/features/agents/ui/PersonaCatalogDialog.tsx b/desktop/src/features/agents/ui/PersonaCatalogDialog.tsx index f8549aca35..e1ec946092 100644 --- a/desktop/src/features/agents/ui/PersonaCatalogDialog.tsx +++ b/desktop/src/features/agents/ui/PersonaCatalogDialog.tsx @@ -277,6 +277,22 @@ function PersonaCatalogChooser({ ); } +/** + * Derives the "Added by" label for a catalog entry from a resolved profile + * summary. Prefers `displayName`, falls back to `name`, then to the default + * "Community member" string when both are absent, null, or whitespace-only. + */ +export function resolveCatalogOwnerLabel( + summary: + | { displayName?: string | null; name?: string | null } + | null + | undefined, +): string { + return ( + summary?.displayName?.trim() || summary?.name?.trim() || "Community member" + ); +} + function PersonaCatalogDetail({ persona }: { persona: AgentPersona }) { const isCommunityEntry = isCatalogPersona(persona) && !persona.catalogSource.isOwn; @@ -294,7 +310,7 @@ function PersonaCatalogDetail({ persona }: { persona: AgentPersona }) { const summary = ownerPubkey ? ownerBatchQuery.data?.profiles[ownerPubkey.toLowerCase()] : undefined; - addedByLabel = summary?.displayName ?? summary?.name ?? "Community member"; + addedByLabel = resolveCatalogOwnerLabel(summary); } return ( diff --git a/desktop/src/features/agents/ui/personaCatalogOwnerLabel.test.mjs b/desktop/src/features/agents/ui/personaCatalogOwnerLabel.test.mjs new file mode 100644 index 0000000000..7ad726352f --- /dev/null +++ b/desktop/src/features/agents/ui/personaCatalogOwnerLabel.test.mjs @@ -0,0 +1,77 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { resolveCatalogOwnerLabel } from "./PersonaCatalogDialog.tsx"; + +// ── null / undefined summary ────────────────────────────────────────────────── + +test("test_null_summary_returns_community_member", () => { + assert.equal(resolveCatalogOwnerLabel(null), "Community member"); +}); + +test("test_undefined_summary_returns_community_member", () => { + assert.equal(resolveCatalogOwnerLabel(undefined), "Community member"); +}); + +// ── populated displayName ───────────────────────────────────────────────────── + +test("test_display_name_present_returns_display_name", () => { + assert.equal( + resolveCatalogOwnerLabel({ displayName: "Alice", name: "alice" }), + "Alice", + ); +}); + +test("test_display_name_present_without_name_returns_display_name", () => { + assert.equal(resolveCatalogOwnerLabel({ displayName: "Alice" }), "Alice"); +}); + +// ── empty / whitespace displayName with valid name ──────────────────────────── + +test("test_empty_display_name_falls_through_to_name", () => { + assert.equal( + resolveCatalogOwnerLabel({ displayName: "", name: "alice" }), + "alice", + ); +}); + +test("test_whitespace_only_display_name_falls_through_to_name", () => { + assert.equal( + resolveCatalogOwnerLabel({ displayName: " ", name: "alice" }), + "alice", + ); +}); + +// ── both candidates absent / empty ──────────────────────────────────────────── + +test("test_both_null_returns_community_member", () => { + assert.equal( + resolveCatalogOwnerLabel({ displayName: null, name: null }), + "Community member", + ); +}); + +test("test_both_empty_returns_community_member", () => { + assert.equal( + resolveCatalogOwnerLabel({ displayName: "", name: "" }), + "Community member", + ); +}); + +test("test_both_whitespace_returns_community_member", () => { + assert.equal( + resolveCatalogOwnerLabel({ displayName: " ", name: "\t" }), + "Community member", + ); +}); + +test("test_display_name_absent_name_present_returns_name", () => { + assert.equal(resolveCatalogOwnerLabel({ name: "alice" }), "alice"); +}); + +test("test_display_name_null_name_present_returns_name", () => { + assert.equal( + resolveCatalogOwnerLabel({ displayName: null, name: "alice" }), + "alice", + ); +});