From 5b53d1f01d1468975ea9b3278cc9c122eb4f309d Mon Sep 17 00:00:00 2001
From: Thomas Petersen
Date: Tue, 4 Aug 2026 16:46:40 +0200
Subject: [PATCH] feat(desktop): show persona duplication count in the catalog
passport
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adding a catalog entry is a local-only write, so true duplication counts
don't exist on the relay — but a member who adds a persona and re-shares
their copy publishes a second catalog head with the same definition.
Count distinct other members with a matching operating prompt and show
it as an honest lower bound ("Duplicated by N other members"). Document
that cross-server runs are invisible from a single relay and what relay
support the full signals would need.
Signed-off-by: Thomas Petersen
---
.../agents/lib/personaAdoption.test.mjs | 82 +++++++++++++++++++
.../features/agents/lib/personaAdoption.ts | 61 ++++++++++++++
.../agents/ui/PersonaCatalogPassport.tsx | 37 ++++++++-
desktop/src/features/passport/BADGES.md | 19 ++++-
.../e2e/persona-catalog-passport.spec.ts | 16 ++++
5 files changed, 211 insertions(+), 4 deletions(-)
create mode 100644 desktop/src/features/agents/lib/personaAdoption.test.mjs
create mode 100644 desktop/src/features/agents/lib/personaAdoption.ts
diff --git a/desktop/src/features/agents/lib/personaAdoption.test.mjs b/desktop/src/features/agents/lib/personaAdoption.test.mjs
new file mode 100644
index 0000000000..de0ad076f8
--- /dev/null
+++ b/desktop/src/features/agents/lib/personaAdoption.test.mjs
@@ -0,0 +1,82 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+ countCatalogRepublishers,
+ personaDefinitionFingerprint,
+} from "./personaAdoption.ts";
+
+const WES = "a".repeat(64);
+const ALICE = "b".repeat(64);
+const BOB = "c".repeat(64);
+
+function publication(ownerPubkey, agent = {}) {
+ return {
+ eventId: "e".repeat(64),
+ ownerPubkey,
+ sourcePersonaId: "persona-1",
+ createdAt: 1_700_000_000,
+ agent: {
+ displayName: "Brain",
+ avatarUrl: null,
+ systemPrompt: "You review pull requests carefully.",
+ runtime: null,
+ model: null,
+ provider: null,
+ namePool: [],
+ respondTo: null,
+ parallelism: null,
+ ...agent,
+ },
+ };
+}
+
+test("fingerprint keys on the prompt, ignoring whitespace and case", () => {
+ const canonical = personaDefinitionFingerprint({
+ displayName: "Brain",
+ systemPrompt: "You review pull requests carefully.",
+ });
+ assert.equal(
+ personaDefinitionFingerprint({
+ displayName: "Renamed Brain",
+ systemPrompt: " You review pull\nrequests CAREFULLY. ",
+ }),
+ canonical,
+ );
+ // Promptless personas fall back to the display name.
+ assert.equal(
+ personaDefinitionFingerprint({ displayName: " Brain ", systemPrompt: "" }),
+ personaDefinitionFingerprint({ displayName: "brain", systemPrompt: " " }),
+ );
+});
+
+test("counts distinct other members with a matching publication", () => {
+ const count = countCatalogRepublishers(
+ [
+ publication(WES), // the original — never counts
+ publication(ALICE),
+ publication(ALICE, { displayName: "Brain (alice)" }), // same member twice
+ publication(BOB, { displayName: "Big Brain" }), // renamed copy, same prompt
+ publication(BOB.replace(/c/gu, "d"), {
+ systemPrompt: "A different persona entirely.",
+ }),
+ ],
+ {
+ displayName: "Brain",
+ systemPrompt: "You review pull requests carefully.",
+ publisherPubkey: WES,
+ },
+ );
+ assert.equal(count, 2);
+});
+
+test("no publisher and no matches yield zero", () => {
+ assert.equal(
+ countCatalogRepublishers([], {
+ displayName: "Brain",
+ systemPrompt: "prompt",
+ publisherPubkey: null,
+ }),
+ 0,
+ );
+});
diff --git a/desktop/src/features/agents/lib/personaAdoption.ts b/desktop/src/features/agents/lib/personaAdoption.ts
new file mode 100644
index 0000000000..78e9543b8b
--- /dev/null
+++ b/desktop/src/features/agents/lib/personaAdoption.ts
@@ -0,0 +1,61 @@
+/**
+ * Adoption signals for a catalog persona, derived from what the relay can
+ * actually show. Adding an entry to your library is a local-only write, so
+ * true duplication counts don't exist on the relay — but a member who adds a
+ * persona and then re-shares their copy publishes a second catalog head with
+ * the same definition. Counting those re-publications gives an honest lower
+ * bound on how many times the persona has been duplicated.
+ */
+
+import type { PersonaCatalogPublication } from "@/features/agents/lib/personaCatalogRelay";
+
+/**
+ * The identity of a persona definition for adoption matching: its operating
+ * prompt when it has one (whitespace-insensitive), otherwise its display name.
+ * Copies keep the source prompt verbatim, so this is the strongest signal a
+ * publication descends from the same definition.
+ */
+export function personaDefinitionFingerprint(definition: {
+ displayName: string;
+ systemPrompt: string;
+}): string {
+ const prompt = definition.systemPrompt.replace(/\s+/gu, " ").trim();
+ if (prompt.length > 0) {
+ return `prompt:${prompt.toLowerCase()}`;
+ }
+ return `name:${definition.displayName.trim().toLowerCase()}`;
+}
+
+/**
+ * Distinct members other than the publisher who have re-published this
+ * persona's definition to the community catalog. A lower bound on adoption:
+ * members who added the persona but never re-shared their copy stay
+ * invisible.
+ */
+export function countCatalogRepublishers(
+ publications: readonly PersonaCatalogPublication[],
+ entry: {
+ displayName: string;
+ systemPrompt: string;
+ publisherPubkey: string | null;
+ },
+): number {
+ const fingerprint = personaDefinitionFingerprint(entry);
+ const publisher = entry.publisherPubkey?.toLowerCase() ?? null;
+ const republishers = new Set();
+ for (const publication of publications) {
+ const owner = publication.ownerPubkey.toLowerCase();
+ if (owner === publisher) {
+ continue;
+ }
+ if (
+ personaDefinitionFingerprint({
+ displayName: publication.agent.displayName,
+ systemPrompt: publication.agent.systemPrompt,
+ }) === fingerprint
+ ) {
+ republishers.add(owner);
+ }
+ }
+ return republishers.size;
+}
diff --git a/desktop/src/features/agents/ui/PersonaCatalogPassport.tsx b/desktop/src/features/agents/ui/PersonaCatalogPassport.tsx
index beeaf59741..afd3ee515f 100644
--- a/desktop/src/features/agents/ui/PersonaCatalogPassport.tsx
+++ b/desktop/src/features/agents/ui/PersonaCatalogPassport.tsx
@@ -1,11 +1,14 @@
import * as React from "react";
import { useNavigate } from "@tanstack/react-router";
-import { IdCard } from "lucide-react";
+import { CopyPlus, IdCard } from "lucide-react";
import {
useManagedAgentsQuery,
useRelayAgentsQuery,
} from "@/features/agents/hooks";
+import { countCatalogRepublishers } from "@/features/agents/lib/personaAdoption";
+import { usePersonaCatalogQuery } from "@/features/agents/lib/usePersonaCatalogRelay";
+import { useCommunities } from "@/features/communities/useCommunities";
import { useAgentPassportBadges } from "@/features/passport/hooks/useAgentPassportBadges";
import { PassportBadgeList } from "@/features/passport/ui/PassportBadges";
import {
@@ -123,6 +126,27 @@ export function PersonaCatalogPassport({
const firstAgent = deployedAgents[0] ?? null;
const { badges } = useAgentPassportBadges(firstAgent?.pubkey ?? null);
+ // Adoption: members who added this persona and re-shared their copy publish
+ // a second catalog head with the same definition. Adds that were never
+ // re-shared are local-only, so this is an honest lower bound — and
+ // cross-server runs are invisible from a single relay entirely.
+ const { activeCommunity } = useCommunities();
+ const catalogQuery = usePersonaCatalogQuery(activeCommunity?.id ?? null);
+ const republisherCount = React.useMemo(
+ () =>
+ countCatalogRepublishers(catalogQuery.data ?? [], {
+ displayName: persona.displayName,
+ publisherPubkey,
+ systemPrompt: persona.systemPrompt,
+ }),
+ [
+ catalogQuery.data,
+ persona.displayName,
+ persona.systemPrompt,
+ publisherPubkey,
+ ],
+ );
+
const navigate = useNavigate();
const openPassport = React.useCallback(
(pubkey: string) => {
@@ -195,6 +219,17 @@ export function PersonaCatalogPassport({
appears here once one is.
)}
+ {republisherCount > 0 ? (
+
+
+ Duplicated by {republisherCount} other{" "}
+ {republisherCount === 1 ? "member" : "members"} — re-shared to
+ this community’s catalog.
+