diff --git a/desktop/src/features/agents/ui/PersonaCatalogDialog.tsx b/desktop/src/features/agents/ui/PersonaCatalogDialog.tsx index ba76d6e4ed..e1ec946092 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"; @@ -276,7 +277,42 @@ 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; + 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 = resolveCatalogOwnerLabel(summary); + } + return (
@@ -290,14 +326,7 @@ function PersonaCatalogDetail({ persona }: { persona: AgentPersona }) { {persona.displayName} {persona.isBuiltIn ? null : ( - + )}
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", + ); +}); 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, }) => {