fix(sidebar): gate working pill on NIP-OA ownership

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
This commit is contained in:
npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
2026-06-30 16:47:41 -07:00
committed by npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f
parent 86d6388e68
commit 07e14d25ad
2 changed files with 175 additions and 9 deletions

View File

@@ -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" },
],
);
});
});

View File

@@ -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<ManagedAgent, "pubkey" | "name">;
type WorkingAgent = Pick<ManagedAgent, "pubkey" | "name" | "status">;
type OwnedRelayWorkingAgent = Pick<RelayAgent, "pubkey" | "name"> & {
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<RelayAgent, "pubkey" | "name">[],
profiles: Record<string, UserProfileSummary> | 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<string>();
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],
);
}