diff --git a/desktop/src/features/agents/agentRelayScope.test.mjs b/desktop/src/features/agents/agentRelayScope.test.mjs index a8ed43837..b625c60ca 100644 --- a/desktop/src/features/agents/agentRelayScope.test.mjs +++ b/desktop/src/features/agents/agentRelayScope.test.mjs @@ -4,8 +4,6 @@ import test from "node:test"; import { agentBelongsToRelay, - countActiveAgentsByCommunity, - hasRunningAgentAnywhere, hasRunningAgentInCommunity, normalizeRelayUrlForCompare, partitionAgentsByRelay, @@ -153,103 +151,3 @@ test("pollingGate_blankPinRunningAgent_polls", () => { true, ); }); - -// ── hasRunningAgentAnywhere: the slow cross-community poll tier ────────────── - -test("anywhereGate_runningAgentInAnyCommunity_polls", () => { - assert.equal( - hasRunningAgentAnywhere([ - { relayUrl: RELAY_A, status: "stopped" }, - { relayUrl: RELAY_B, status: "running" }, - ]), - true, - ); -}); - -test("anywhereGate_deployedOnly_doesNotPoll", () => { - // Provider-backed agents change status only through control-plane - // actions — no silent process death to notice, so no poll. - assert.equal( - hasRunningAgentAnywhere([{ relayUrl: RELAY_A, status: "deployed" }]), - false, - ); - assert.equal(hasRunningAgentAnywhere(undefined), false); -}); - -// ── countActiveAgentsByCommunity: the community-rail active-agents dot ─────── - -const COMMUNITIES = [ - { id: "community-a", relayUrl: RELAY_A }, - { id: "community-b", relayUrl: RELAY_B }, -]; - -test("railCounts_scopesActiveAgentsToTheirPinnedCommunity", () => { - const counts = countActiveAgentsByCommunity( - [ - { relayUrl: RELAY_A, status: "running" }, - // Cosmetic URL differences must not split an agent from its community. - { relayUrl: `WS://RELAY-A.example.com:3000/`, status: "running" }, - { relayUrl: RELAY_B, status: "stopped" }, - ], - COMMUNITIES, - "community-b", - ); - - assert.equal(counts.get("community-a"), 2); - assert.equal(counts.get("community-b"), undefined); -}); - -test("railCounts_deployedCountsAsActive", () => { - // Mirrors isManagedAgentActive: the agents screen presents deployed - // provider-backed agents as active, so the rail dot must agree. - const counts = countActiveAgentsByCommunity( - [{ relayUrl: RELAY_B, status: "deployed" }], - COMMUNITIES, - "community-a", - ); - - assert.equal(counts.get("community-b"), 1); -}); - -test("railCounts_blankPin_followsActiveCommunityOnly", () => { - // On an all-communities surface the per-surface "blank follows the - // community being viewed" fallback would light EVERY dot for one stray - // unstamped record; here it must attach to the active community alone. - const counts = countActiveAgentsByCommunity( - [{ relayUrl: "", status: "running" }], - COMMUNITIES, - "community-a", - ); - - assert.equal(counts.get("community-a"), 1); - assert.equal(counts.get("community-b"), undefined); -}); - -test("railCounts_blankPin_noActiveCommunity_countsNowhere", () => { - const counts = countActiveAgentsByCommunity( - [{ relayUrl: "", status: "running" }], - COMMUNITIES, - null, - ); - - assert.equal(counts.size, 0); -}); - -test("railCounts_communitiesSharingARelay_bothLight", () => { - const counts = countActiveAgentsByCommunity( - [{ relayUrl: RELAY_A, status: "running" }], - [ - { id: "community-a", relayUrl: RELAY_A }, - { id: "community-a-alias", relayUrl: `${RELAY_A}/` }, - ], - "community-a", - ); - - assert.equal(counts.get("community-a"), 1); - assert.equal(counts.get("community-a-alias"), 1); -}); - -test("railCounts_undefinedAgents_yieldsEmpty", () => { - const counts = countActiveAgentsByCommunity(undefined, COMMUNITIES, null); - assert.equal(counts.size, 0); -}); diff --git a/desktop/src/features/agents/agentRelayScope.ts b/desktop/src/features/agents/agentRelayScope.ts index e0953587e..8092b188e 100644 --- a/desktop/src/features/agents/agentRelayScope.ts +++ b/desktop/src/features/agents/agentRelayScope.ts @@ -98,64 +98,3 @@ export function hasRunningAgentInCommunity( agentBelongsToRelay(agent.relayUrl, communityRelayUrl), ); } - -/** - * Whether any local agent process is running, in ANY community. Drives the - * slow cross-community poll tier: all-communities surfaces (the community - * rail's active-agents dot, the "running in other communities" line) render - * last-known process state for backgrounded communities, so a relaxed poll - * must keep that state from going permanently stale when a background - * process dies. Deployed (provider-backed) agents are excluded — their - * status changes only through control-plane actions, never silently. - */ -export function hasRunningAgentAnywhere( - agents: readonly { status: string }[] | undefined, -): boolean { - return (agents ?? []).some((agent) => agent.status === "running"); -} - -/** - * Active (running or deployed — mirrors `isManagedAgentActive`) managed - * agents per community id, for surfaces that render ALL communities at once - * (the community rail). - * - * A pinned agent counts toward every community whose relay normalizes to - * its pin — two rail entries pointing at the same relay genuinely share - * their agents. A blank pin counts toward the ACTIVE community only: the - * per-surface `agentBelongsToRelay` fallback ("blank follows the community - * being viewed") would light every community for one stray unstamped - * record when evaluated against all of them side by side. - */ -export function countActiveAgentsByCommunity( - agents: readonly { relayUrl?: string | null; status: string }[] | undefined, - communities: readonly { id: string; relayUrl: string }[], - activeCommunityId: string | null, -): Map { - const counts = new Map(); - const idsByRelay = new Map(); - for (const community of communities) { - const key = normalizeRelayUrlForCompare(community.relayUrl); - const ids = idsByRelay.get(key); - if (ids) { - ids.push(community.id); - } else { - idsByRelay.set(key, [community.id]); - } - } - for (const agent of agents ?? []) { - if (agent.status !== "running" && agent.status !== "deployed") { - continue; - } - const pinned = agent.relayUrl?.trim() ?? ""; - const ids = - pinned === "" - ? activeCommunityId !== null - ? [activeCommunityId] - : [] - : (idsByRelay.get(normalizeRelayUrlForCompare(pinned)) ?? []); - for (const id of ids) { - counts.set(id, (counts.get(id) ?? 0) + 1); - } - } - return counts; -} diff --git a/desktop/src/features/agents/hooks.ts b/desktop/src/features/agents/hooks.ts index dd7f0f59b..b46cf0747 100644 --- a/desktop/src/features/agents/hooks.ts +++ b/desktop/src/features/agents/hooks.ts @@ -1,10 +1,7 @@ import * as React from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { - hasRunningAgentAnywhere, - hasRunningAgentInCommunity, -} from "@/features/agents/agentRelayScope"; +import { hasRunningAgentInCommunity } from "@/features/agents/agentRelayScope"; import { attachManagedAgentToChannel, createChannelManagedAgents, @@ -297,18 +294,11 @@ export function useManagedAgentsQuery(options?: { enabled?: boolean }) { // with no relay event to signal it, so this poll is the only liveness // path for them. When nothing is running there IS an event path — // `agents-data-changed` (control-plane changes) — so the idle branch - // drops its poll entirely rather than falling back to 30s. Two tiers: - // 5s while an agent in the ACTIVE community runs (its rows render live - // process state), relaxed to 60s while agents run only in other - // communities — those surface merely as last-known indicators (the - // community rail's active-agents dot, the "running in other - // communities" line), which must still notice a background process - // dying rather than staying green forever. - return hasRunningAgentInCommunity(agents, activeRelayUrl) - ? 5_000 - : hasRunningAgentAnywhere(agents) - ? 60_000 - : false; + // drops its poll entirely rather than falling back to 30s. Scoped to + // the active community's relay: agents left running in other + // communities render no process state on this community's surfaces, + // so they must not keep its poll alive. + return hasRunningAgentInCommunity(agents, activeRelayUrl) ? 5_000 : false; }, }); } diff --git a/desktop/src/features/sidebar/ui/CommunityRail.test.mjs b/desktop/src/features/sidebar/ui/CommunityRail.test.mjs index fe6d75804..8a8b33b15 100644 --- a/desktop/src/features/sidebar/ui/CommunityRail.test.mjs +++ b/desktop/src/features/sidebar/ui/CommunityRail.test.mjs @@ -1,10 +1,7 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; -import { - communityRailIndicators, - communityRailTooltipLabel, -} from "./CommunityRail.tsx"; +import { communityRailIndicators } from "./CommunityRail.tsx"; describe("communityRailIndicators", () => { it("shows no badge for an observed community with unread but no mentions", () => { @@ -73,55 +70,3 @@ describe("communityRailIndicators", () => { assert.equal(r.pending, false); }); }); - -describe("communityRailTooltipLabel", () => { - const quiet = { showBadge: false, showDot: false, mentionCount: 0 }; - - it("is just the name with nothing to report", () => { - assert.equal(communityRailTooltipLabel("Acme", quiet, 0), "Acme"); - }); - - it("reports active agents with singular/plural forms", () => { - assert.equal( - communityRailTooltipLabel("Acme", quiet, 1), - "Acme — 1 agent active", - ); - assert.equal( - communityRailTooltipLabel("Acme", quiet, 3), - "Acme — 3 agents active", - ); - }); - - it("combines mentions with active agents — mentions beat plain unread", () => { - assert.equal( - communityRailTooltipLabel( - "Acme", - { showBadge: true, showDot: false, mentionCount: 2 }, - 1, - ), - "Acme — 2 mentions, 1 agent active", - ); - }); - - it("combines plain unread with active agents", () => { - assert.equal( - communityRailTooltipLabel( - "Acme", - { showBadge: false, showDot: true, mentionCount: 0 }, - 2, - ), - "Acme — unread, 2 agents active", - ); - }); - - it("keeps the plain unread form without agents", () => { - assert.equal( - communityRailTooltipLabel( - "Acme", - { showBadge: false, showDot: true, mentionCount: 0 }, - 0, - ), - "Acme — unread", - ); - }); -}); diff --git a/desktop/src/features/sidebar/ui/CommunityRail.tsx b/desktop/src/features/sidebar/ui/CommunityRail.tsx index 2ce211a5d..57b7ed4d7 100644 --- a/desktop/src/features/sidebar/ui/CommunityRail.tsx +++ b/desktop/src/features/sidebar/ui/CommunityRail.tsx @@ -1,8 +1,6 @@ import { CheckCheck, Link2, Plus, Settings2 } from "lucide-react"; import * as React from "react"; -import { countActiveAgentsByCommunity } from "@/features/agents/agentRelayScope"; -import { useManagedAgentsQuery } from "@/features/agents/hooks"; import type { Community } from "@/features/communities/types"; import { EditCommunityDialog } from "@/features/communities/ui/EditCommunityDialog"; import { useCommunityIcons } from "@/features/communities/useCommunityIcons"; @@ -70,41 +68,10 @@ export function communityRailIndicators(unread: CommunityUnreadState): { }; } -/** - * Tooltip / aria label for one community button: the community name, plus - * the unread state (mention count beats plain unread, mirroring the badge/dot - * exclusivity) and the active-agent count when agents run there. Pure for - * unit tests. - */ -export function communityRailTooltipLabel( - name: string, - indicators: Pick< - ReturnType, - "showBadge" | "showDot" | "mentionCount" - >, - activeAgentCount: number, -): string { - const parts: string[] = []; - if (indicators.showBadge) { - parts.push( - `${indicators.mentionCount} mention${indicators.mentionCount === 1 ? "" : "s"}`, - ); - } else if (indicators.showDot) { - parts.push("unread"); - } - if (activeAgentCount > 0) { - parts.push( - `${activeAgentCount} agent${activeAgentCount === 1 ? "" : "s"} active`, - ); - } - return parts.length === 0 ? name : `${name} — ${parts.join(", ")}`; -} - function CommunityButton({ community, isActive, unread, - activeAgentCount, iconUrl, onSwitch, menu, @@ -112,19 +79,18 @@ function CommunityButton({ community: Community; isActive: boolean; unread: CommunityUnreadState; - activeAgentCount: number; iconUrl: string | null; onSwitch: () => void; menu: React.ReactNode; }) { - const indicators = communityRailIndicators(unread); - const { showBadge, showDot, pending, badgeLabel } = indicators; + const { mentionCount, showBadge, showDot, pending, badgeLabel } = + communityRailIndicators(unread); - const tooltipLabel = communityRailTooltipLabel( - community.name, - indicators, - activeAgentCount, - ); + const tooltipLabel = showBadge + ? `${community.name} — ${mentionCount} mention${mentionCount === 1 ? "" : "s"}` + : showDot + ? `${community.name} — unread` + : community.name; return ( @@ -160,17 +126,6 @@ function CommunityButton({ getInitials(community.name) || "🐝" )} - {activeAgentCount > 0 ? ( - // Top-right so it composes with the unread badge/dot at - // bottom-right. Emerald mirrors the agents list's "online" - // PresenceDot (getPresenceDotClassName). - - agents active - - ) : null} {showBadge ? (