Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 37 additions & 8 deletions desktop/src/features/agents/ui/PersonaCatalogDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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">
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
);
});
36 changes: 35 additions & 1 deletion desktop/tests/e2e/agents.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
}) => {
Expand Down
Loading