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,
}) => {