From 07e14d25ad8d435fb1be7b0b3ca2ab69229570cd Mon Sep 17 00:00:00 2001 From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co> Date: Tue, 30 Jun 2026 16:47:41 -0700 Subject: [PATCH] fix(sidebar): gate working pill on NIP-OA ownership Co-authored-by: Taylor Ho Signed-off-by: Taylor Ho --- .../lib/useActiveWorkingChannelsById.test.mjs | 91 +++++++++++++++++- .../lib/useActiveWorkingChannelsById.ts | 93 +++++++++++++++++-- 2 files changed, 175 insertions(+), 9 deletions(-) diff --git a/desktop/src/features/sidebar/lib/useActiveWorkingChannelsById.test.mjs b/desktop/src/features/sidebar/lib/useActiveWorkingChannelsById.test.mjs index 5f022df53a..5593b6a77b 100644 --- a/desktop/src/features/sidebar/lib/useActiveWorkingChannelsById.test.mjs +++ b/desktop/src/features/sidebar/lib/useActiveWorkingChannelsById.test.mjs @@ -1,10 +1,19 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; -import { resolveActiveWorkingChannelNames } from "./useActiveWorkingChannelsById.ts"; +import { + getOwnedRelayWorkingAgents, + mergeWorkingAgents, + resolveActiveWorkingChannelNames, +} from "./useActiveWorkingChannelsById.ts"; + +const VIEWER_PUBKEY = + "80c5f18be5aafa62cf6198c6335963ba3306b595288117c8ea2f805fc9bdc94a"; +const OWNED_RELAY_AGENT_PUBKEY = + "a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff00"; describe("resolveActiveWorkingChannelNames", () => { - it("resolves active agent pubkeys to managed agent names", () => { + it("resolves active agent pubkeys to working agent names", () => { const resolved = resolveActiveWorkingChannelNames( { channelId: "chan-1", @@ -34,4 +43,82 @@ describe("resolveActiveWorkingChannelNames", () => { assert.deepEqual(resolved.agentNames, ["Ned"]); }); + + it("resolves owned relay agent names", () => { + const resolved = resolveActiveWorkingChannelNames( + { + channelId: "chan-1", + anchorAt: 0, + agentCount: 1, + agentPubkeys: [OWNED_RELAY_AGENT_PUBKEY.toUpperCase()], + }, + [{ pubkey: OWNED_RELAY_AGENT_PUBKEY, name: "nadia" }], + ); + + assert.deepEqual(resolved.agentNames, ["nadia"]); + }); +}); + +describe("getOwnedRelayWorkingAgents", () => { + it("keeps relay agents whose NIP-OA owner is the current viewer", () => { + assert.deepEqual( + getOwnedRelayWorkingAgents( + [ + { pubkey: OWNED_RELAY_AGENT_PUBKEY, name: "nadia" }, + { pubkey: "other-agent", name: "nelson" }, + ], + { + [OWNED_RELAY_AGENT_PUBKEY]: { + displayName: "nadia", + avatarUrl: null, + nip05Handle: null, + ownerPubkey: VIEWER_PUBKEY.toUpperCase(), + isAgent: true, + }, + "other-agent": { + displayName: "nelson", + avatarUrl: null, + nip05Handle: null, + ownerPubkey: "someone-else", + isAgent: true, + }, + }, + VIEWER_PUBKEY, + ), + [{ pubkey: OWNED_RELAY_AGENT_PUBKEY, name: "nadia", status: "deployed" }], + ); + }); + + it("returns no relay agents without a current viewer", () => { + assert.deepEqual( + getOwnedRelayWorkingAgents( + [{ pubkey: OWNED_RELAY_AGENT_PUBKEY, name: "nadia" }], + {}, + undefined, + ), + [], + ); + }); +}); + +describe("mergeWorkingAgents", () => { + it("dedupes owned relay agents behind locally managed agents", () => { + assert.deepEqual( + mergeWorkingAgents( + [{ pubkey: "AAAA", name: "Ned", status: "running" }], + [ + { pubkey: "aaaa", name: "Relay Ned", status: "deployed" }, + { + pubkey: OWNED_RELAY_AGENT_PUBKEY, + name: "nadia", + status: "deployed", + }, + ], + ), + [ + { pubkey: "AAAA", name: "Ned", status: "running" }, + { pubkey: OWNED_RELAY_AGENT_PUBKEY, name: "nadia", status: "deployed" }, + ], + ); + }); }); diff --git a/desktop/src/features/sidebar/lib/useActiveWorkingChannelsById.ts b/desktop/src/features/sidebar/lib/useActiveWorkingChannelsById.ts index 03f245f64a..ec514c7c5b 100644 --- a/desktop/src/features/sidebar/lib/useActiveWorkingChannelsById.ts +++ b/desktop/src/features/sidebar/lib/useActiveWorkingChannelsById.ts @@ -5,16 +5,32 @@ import { useActiveAgentTurnsBridge, useActiveAgentTurnsByChannel, } from "@/features/agents/activeAgentTurnsStore"; -import { useManagedAgentsQuery } from "@/features/agents/hooks"; +import { + useManagedAgentsQuery, + useRelayAgentsQuery, +} from "@/features/agents/hooks"; import { useManagedAgentObserverBridge } from "@/features/agents/observerRelayStore"; +import { useUsersBatchQuery } from "@/features/profile/hooks"; +import { useIdentityQuery } from "@/shared/api/hooks"; +import type { + ManagedAgent, + RelayAgent, + UserProfileSummary, +} from "@/shared/api/types"; import { normalizePubkey } from "@/shared/lib/pubkey"; +type WorkingAgentName = Pick; +type WorkingAgent = Pick; +type OwnedRelayWorkingAgent = Pick & { + status: "deployed"; +}; + export function resolveActiveWorkingChannelNames( summary: ActiveChannelTurnSummary, - managedAgents: readonly { pubkey: string; name: string }[], + workingAgents: readonly WorkingAgentName[], ): ActiveChannelTurnSummary { const namesByPubkey = new Map( - managedAgents.map((agent) => [normalizePubkey(agent.pubkey), agent.name]), + workingAgents.map((agent) => [normalizePubkey(agent.pubkey), agent.name]), ); return { @@ -26,18 +42,81 @@ export function resolveActiveWorkingChannelNames( }; } +export function getOwnedRelayWorkingAgents( + relayAgents: readonly Pick[], + profiles: Record | undefined, + currentPubkey: string | undefined, +): OwnedRelayWorkingAgent[] { + if (!currentPubkey) return []; + + const normalizedCurrentPubkey = normalizePubkey(currentPubkey); + return relayAgents.flatMap((agent) => { + const profile = profiles?.[normalizePubkey(agent.pubkey)]; + if (!profile?.ownerPubkey) return []; + if (normalizePubkey(profile.ownerPubkey) !== normalizedCurrentPubkey) { + return []; + } + + return [{ pubkey: agent.pubkey, name: agent.name, status: "deployed" }]; + }); +} + +export function mergeWorkingAgents( + managedAgents: readonly WorkingAgent[], + ownedRelayAgents: readonly WorkingAgent[], +): WorkingAgent[] { + const seenPubkeys = new Set(); + const merged: WorkingAgent[] = []; + + for (const agent of [...managedAgents, ...ownedRelayAgents]) { + const pubkey = normalizePubkey(agent.pubkey); + if (seenPubkeys.has(pubkey)) continue; + seenPubkeys.add(pubkey); + merged.push(agent); + } + + return merged; +} + export function useActiveWorkingChannelsById(): ReadonlyMap< string, ActiveChannelTurnSummary > { + const identityQuery = useIdentityQuery(); + const currentPubkey = identityQuery.data?.pubkey; const managedAgentsQuery = useManagedAgentsQuery(); const managedAgents = React.useMemo( () => managedAgentsQuery.data ?? [], [managedAgentsQuery.data], ); + const relayAgentsQuery = useRelayAgentsQuery(); + const relayAgents = React.useMemo( + () => relayAgentsQuery.data ?? [], + [relayAgentsQuery.data], + ); + const relayAgentPubkeys = React.useMemo( + () => relayAgents.map((agent) => agent.pubkey), + [relayAgents], + ); + const relayAgentProfilesQuery = useUsersBatchQuery(relayAgentPubkeys, { + enabled: relayAgentPubkeys.length > 0, + }); + const ownedRelayAgents = React.useMemo( + () => + getOwnedRelayWorkingAgents( + relayAgents, + relayAgentProfilesQuery.data?.profiles, + currentPubkey, + ), + [currentPubkey, relayAgentProfilesQuery.data?.profiles, relayAgents], + ); + const workingAgents = React.useMemo( + () => mergeWorkingAgents(managedAgents, ownedRelayAgents), + [managedAgents, ownedRelayAgents], + ); - useManagedAgentObserverBridge(managedAgents); - useActiveAgentTurnsBridge(managedAgents); + useManagedAgentObserverBridge(workingAgents); + useActiveAgentTurnsBridge(workingAgents); const activeWorkingChannels = useActiveAgentTurnsByChannel(); return React.useMemo( @@ -46,11 +125,11 @@ export function useActiveWorkingChannelsById(): ReadonlyMap< activeWorkingChannels.map((summary) => { const resolvedSummary = resolveActiveWorkingChannelNames( summary, - managedAgents, + workingAgents, ); return [resolvedSummary.channelId, resolvedSummary]; }), ), - [activeWorkingChannels, managedAgents], + [activeWorkingChannels, workingAgents], ); }