Skip to content
Open
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
18 changes: 18 additions & 0 deletions desktop/src/features/agents/lib/pickProfileAgent.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@ test("the shared profile target prefers the active persona instance", () => {
assert.equal(pickProfileAgent([running, stopped], NONE_ARCHIVED), running);
});

test("the active community identity wins over a running sibling", () => {
const legacy = agent({
pubkey: "a".repeat(64),
relayUrl: "wss://legacy.example",
status: "running",
});
const current = agent({
pubkey: "b".repeat(64),
relayUrl: "wss://current.example",
status: "stopped",
});

assert.equal(
pickProfileAgent([legacy, current], NONE_ARCHIVED, "wss://current.example"),
current,
);
});

test("an archived instance early in file order cannot hijack the target", () => {
const archived = agent({
name: "Archived instance",
Expand Down
9 changes: 8 additions & 1 deletion desktop/src/features/agents/lib/pickProfileAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,22 @@ import type { ManagedAgent } from "@/shared/api/types";
* file order can't hijack the persona target. Returns `undefined` when every
* instance is archived — the card then renders in persona-only mode. The
* `isArchived` predicate is fail-open (returns `false` while the relay archive
* snapshot loads), so a cold start never briefly picks nothing.
* snapshot loads), so a cold start never briefly picks nothing. When the same
* persona has identities from several communities, the active community's
* identity wins before runtime status.
*/
export function pickProfileAgent(
agents: readonly ManagedAgent[],
isArchived: (pubkey: string) => boolean,
preferredRelayUrl?: string,
) {
return [...agents]
.filter((agent) => !isArchived(agent.pubkey))
.sort((left, right) => {
const relayDiff =
Number(right.relayUrl === preferredRelayUrl) -
Number(left.relayUrl === preferredRelayUrl);
if (relayDiff !== 0) return relayDiff;
const activeDiff =
Number(isManagedAgentActive(right)) -
Number(isManagedAgentActive(left));
Expand Down
8 changes: 7 additions & 1 deletion desktop/src/features/agents/ui/UnifiedAgentsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { resolveAgentCardModelLabel } from "@/features/agents/lib/agentCardModel
import { friendlyAgentLastError } from "@/features/agents/lib/friendlyAgentLastError";
import { isManagedAgentActive } from "@/features/agents/lib/managedAgentControlActions";
import { pickProfileAgent } from "@/features/agents/lib/pickProfileAgent";
import { useCommunities } from "@/features/communities/useCommunities";
import { useIsArchivedPredicate } from "@/features/identity-archive/hooks";
import { useUserProfileQuery } from "@/features/profile/hooks";
import type { AgentPersona, ManagedAgent } from "@/shared/api/types";
Expand Down Expand Up @@ -95,6 +96,7 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
onDeletePersona,
} = props;

const { activeCommunity } = useCommunities();
const isArchived = useIsArchivedPredicate();
const { groups, ungrouped, unknown } = React.useMemo(
() => buildUnifiedGroups(personas, agents, isArchived),
Expand Down Expand Up @@ -131,7 +133,11 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
onClick={onOpenCatalog}
/>
{groups.map((group) => {
const profileAgent = pickProfileAgent(group.agents, isArchived);
const profileAgent = pickProfileAgent(
group.agents,
isArchived,
activeCommunity?.relayUrl,
);
return (
<AgentPersonaCard
actions={(effectiveAvatarUrl, isEffectiveAvatarLoading) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ let screen;
let createElement;
let QueryClient;
let QueryClientProvider;
let CommunitiesProvider;
let UnifiedAgentsSection;

const ipcHandlers = new Map();
Expand Down Expand Up @@ -118,7 +119,11 @@ function renderSection(props) {
createElement(
QueryClientProvider,
{ client },
createElement(UnifiedAgentsSection, props),
createElement(
CommunitiesProvider,
null,
createElement(UnifiedAgentsSection, props),
),
),
);
}
Expand All @@ -127,6 +132,7 @@ before(async () => {
Object.assign(globalThis, {
document: dom.window.document,
HTMLElement: dom.window.HTMLElement,
localStorage: dom.window.localStorage,
window: dom.window,
IS_REACT_ACT_ENVIRONMENT: true,
});
Expand Down Expand Up @@ -156,6 +162,9 @@ before(async () => {
({ QueryClient, QueryClientProvider } = await import(
"@tanstack/react-query"
));
({ CommunitiesProvider } = await import(
"@/features/communities/useCommunities"
));
({ UnifiedAgentsSection } = await import("./UnifiedAgentsSection.tsx"));
});

Expand Down
16 changes: 14 additions & 2 deletions desktop/src/features/agents/ui/useManagedAgentActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { useGlobalAgentConfig } from "@/features/agents/useGlobalAgentConfig";
import { useChannelsQuery } from "@/features/channels/hooks";
import { invalidateChannelMembersRosters } from "@/features/channels/rosterFreshness";
import { useCommunities } from "@/features/communities/useCommunities";
import { usePresenceQuery } from "@/features/presence/hooks";
import type { AgentPersona, Channel, ManagedAgent } from "@/shared/api/types";
import { removeChannelMember } from "@/shared/api/tauri";
Expand All @@ -38,6 +39,7 @@ import {
export function useManagedAgentActions() {
const queryClient = useQueryClient();
const { globalConfig } = useGlobalAgentConfig();
const { activeCommunity } = useCommunities();
const relayAgentsQuery = useRelayAgentsQuery();
const managedAgentsQuery = useManagedAgentsQuery();
const [shouldLoadChannels, setShouldLoadChannels] = React.useState(false);
Expand Down Expand Up @@ -166,7 +168,12 @@ export function useManagedAgentActions() {
if (!agent) return;
await startManagedAgentWithRules({
agent,
startManagedAgent: startMutation.mutateAsync,
startManagedAgent: (agentPubkey) =>
startMutation.mutateAsync({
pubkey: agentPubkey,
expectedRelayUrl: activeCommunity?.relayUrl,
expectedSignerPubkey: activeCommunity?.pubkey,
}),
});
} catch (error) {
setActionErrorMessage(
Expand All @@ -186,7 +193,12 @@ export function useManagedAgentActions() {
if (!agent) return;
await respawnManagedAgentWithRules({
agent,
startManagedAgent: startMutation.mutateAsync,
startManagedAgent: (agentPubkey) =>
startMutation.mutateAsync({
pubkey: agentPubkey,
expectedRelayUrl: activeCommunity?.relayUrl,
expectedSignerPubkey: activeCommunity?.pubkey,
}),
stopManagedAgent: stopMutation.mutateAsync,
onStopped: () => clearActiveTurnsForAgentOnStop(agent.pubkey),
});
Expand Down