feat(catalog): resolve publisher display name in catalog detail pane (#3640)

The catalog detail pane hardcoded "Community member" for every non-own
catalog entry. The publisher pubkey (`catalogSource.ownerPubkey`) was
already on every entry — it just was not being resolved to a name.

## What changed

**`desktop/src/features/agents/ui/PersonaCatalogDialog.tsx`**

`PersonaCatalogDetail` now calls `useUsersBatchQuery([ownerPubkey])`
when the selected entry is a community (non-own) catalog agent. The
label derivation is extracted into the exported pure function
`resolveCatalogOwnerLabel` and uses truthy fallbacks to handle empty or
whitespace-only kind:0 fields:

- Own entry → `"You"` (unchanged)
- `displayName` present and non-blank → the display name
- `displayName` absent/blank but `name` present and non-blank → the name
- Loading, unresolvable, or both candidates blank → `"Community member"`
(fallback preserved)

The batch query is disabled (`enabled: false`) when the entry is not a
community entry, so there is no extra network call for own entries or
built-in agents.

**`desktop/src/features/agents/ui/personaCatalogOwnerLabel.test.mjs`**

Unit tests for `resolveCatalogOwnerLabel` covering: populated
`displayName` wins; whitespace-only `displayName` falls through to
`name`; both candidates empty/whitespace/null/undefined all fall through
to `"Community member"`.

**`desktop/tests/e2e/agents.spec.ts`**

- Updated the existing assertion — it previously checked for the
hardcoded fallback; now asserts the resolved mock display name
`"alice"`.
- Added "catalog detail shows Community member when the publisher
profile cannot be resolved" — installs a catalog event from an unknown
pubkey and asserts the fallback still renders.

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
This commit is contained in:
Will Pfleger
2026-07-30 11:38:28 -04:00
committed by GitHub
co-authored by npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw
parent 4933672eb4
commit 02be413b82
3 changed files with 149 additions and 9 deletions
@@ -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 (
<div className="w-full min-w-0 max-w-full space-y-6 overflow-x-hidden">
<div className="flex items-center gap-3">
@@ -290,14 +326,7 @@ function PersonaCatalogDetail({ persona }: { persona: AgentPersona }) {
{persona.displayName}
</h3>
{persona.isBuiltIn ? null : (
<PersonaAddedBy
className="mt-0.5"
label={
isCatalogPersona(persona) && !persona.catalogSource.isOwn
? "Community member"
: "You"
}
/>
<PersonaAddedBy className="mt-0.5" label={addedByLabel} />
)}
</div>
</div>
@@ -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",
);
});
+35 -1
View File
@@ -1736,8 +1736,10 @@ test("a community member can discover and add another member's catalog agent", a
);
await expect(remoteEntry).toContainText("Alices 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
@@ -1790,6 +1792,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,
}) => {