revert(desktop): remove the community-rail active-agents dot

Revert 0f8fa7e1841cf49a33e0e4c7158cb6e54ff762a1 ("feat(desktop): show an
active-agents dot on the community rail"), backing out the rail status
dot and its supporting plumbing:

- countActiveAgentsByCommunity / hasRunningAgentAnywhere leave
  agentRelayScope, the dot and communityRailTooltipLabel leave
  CommunityRail, the slow 60s cross-community tier leaves the
  useManagedAgentsQuery poll gate, and the E2E bridge drops the
  managed-agent relayUrl seed plus the rail dot spec test.

One deliberate deviation from a pure revert: playwright.config.ts keeps
matching community-rail.spec.ts. The reverted commit had fixed a stale
workspace-rail.spec.ts testMatch left over from the spec's rename;
restoring that line would silently de-register the spec's seven
remaining pre-existing tests, which are unrelated to the dot feature.

The later hardening commit a4974089's fixture-agreement test and
normalizer doc comments in agentRelayScope are untouched by the revert.

Tested: desktop pnpm test (2942 passed), tsc --noEmit, biome check,
Playwright community-rail spec (7 passed) against a fresh build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
This commit is contained in:
Matt Toohey
2026-07-16 23:58:10 +10:00
co-authored by Claude Fable 5
parent 1712cbd2ed
commit 87e5b6d172
8 changed files with 17 additions and 354 deletions
@@ -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);
});
@@ -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<string, number> {
const counts = new Map<string, number>();
const idsByRelay = new Map<string, string[]>();
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;
}
+6 -16
View File
@@ -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;
},
});
}
@@ -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",
);
});
});
@@ -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<typeof communityRailIndicators>,
"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 (
<ContextMenu modal={false}>
@@ -160,17 +126,6 @@ function CommunityButton({
getInitials(community.name) || "🐝"
)}
</span>
{activeAgentCount > 0 ? (
// Top-right so it composes with the unread badge/dot at
// bottom-right. Emerald mirrors the agents list's "online"
// PresenceDot (getPresenceDotClassName).
<span
className="absolute -right-0.5 -top-0.5 h-2 w-2 shrink-0 rounded-full bg-emerald-500 ring-2 ring-sidebar"
data-testid={`community-rail-agents-dot-${community.id}`}
>
<span className="sr-only">agents active</span>
</span>
) : null}
{showBadge ? (
<span
className="absolute -bottom-0.5 -right-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-2xs font-semibold text-primary-foreground ring-2 ring-sidebar"
@@ -201,10 +156,8 @@ function CommunityButton({
/**
* Discord/Slack-style vertical rail of communities on the far left of the app.
* Shows a mention-count badge for inactive communities (observed via
* `useCommunityUnread`), a green dot on communities with active managed
* agents (running there thanks to lazy multi-workspace activation), and
* switches relays on click. Right-click opens a per-community menu: mark all
* as read, copy relay URL, community settings.
* `useCommunityUnread`) and switches relays on click. Right-click opens a
* per-community menu: mark all as read, copy relay URL, community settings.
*
* Hidden entirely with a single community — a rail of one adds no value.
*/
@@ -220,19 +173,6 @@ export function CommunityRail({
communities,
activeCommunityId,
);
// Shares the app-wide managed-agents cache; process-state freshness for
// other communities rides the slow poll tier in useManagedAgentsQuery.
const managedAgentsQuery = useManagedAgentsQuery();
const managedAgents = managedAgentsQuery.data;
const activeAgentCounts = React.useMemo(
() =>
countActiveAgentsByCommunity(
managedAgents,
communities,
activeCommunityId,
),
[managedAgents, communities, activeCommunityId],
);
const iconsByCommunity = useCommunityIcons(communities);
const isFullscreen = useIsFullscreen();
const { markAllChannelsRead } = useAppShell();
@@ -273,7 +213,6 @@ export function CommunityRail({
{communities.map((community) => (
<CommunityButton
key={community.id}
activeAgentCount={activeAgentCounts.get(community.id) ?? 0}
iconUrl={iconsByCommunity[community.id] ?? null}
isActive={community.id === activeCommunityId}
menu={
+1 -3
View File
@@ -64,8 +64,6 @@ type MockManagedAgentSeed = {
avatarUrl?: string | null;
personaId?: string | null;
status?: RawManagedAgent["status"];
/** Home-relay pin; defaults to the mock workspace relay when omitted. */
relayUrl?: string;
channelNames?: string[];
channelIds?: string[];
backend?: RawManagedAgent["backend"];
@@ -1752,7 +1750,7 @@ function buildSeededManagedAgent(seed: MockManagedAgentSeed): MockManagedAgent {
pubkey: seed.pubkey,
name: seed.name,
persona_id: seed.personaId ?? null,
relay_url: seed.relayUrl ?? DEFAULT_RELAY_WS_URL,
relay_url: DEFAULT_RELAY_WS_URL,
acp_command: "buzz-acp",
agent_command: "goose",
agent_args: ["acp"],
-44
View File
@@ -134,50 +134,6 @@ test.describe("community rail", () => {
await expect(buttonB).toHaveAttribute("aria-current", "true");
});
test("shows an active-agents dot only on communities with running agents", async ({
page,
}) => {
await installMockBridge(
page,
{
managedAgents: [
{
pubkey:
"aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11aa11",
name: "Bravo Coder",
status: "running",
relayUrl: COMMUNITY_B.relayUrl,
},
{
pubkey:
"bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22",
name: "Alpha Idle",
status: "stopped",
relayUrl: COMMUNITY_A.relayUrl,
},
],
},
{ skipCommunitySeed: true },
);
await seedCommunities(page, [COMMUNITY_A, COMMUNITY_B], COMMUNITY_A.id);
await page.goto("/");
// The community with a running agent gets the green dot — even while
// another community is active — and its tooltip label reports the count.
const dotB = page.getByTestId(
`community-rail-agents-dot-${COMMUNITY_B.id}`,
);
await expect(dotB).toBeVisible();
await expect(
page.getByTestId(`community-rail-button-${COMMUNITY_B.id}`),
).toHaveAttribute("aria-label", "Bravo — 1 agent active");
// A community with only stopped agents shows no dot.
await expect(
page.getByTestId(`community-rail-agents-dot-${COMMUNITY_A.id}`),
).toHaveCount(0);
});
test("hides the rail with a single community", async ({ page }) => {
await installMockBridge(page, undefined, { skipCommunitySeed: true });
await seedCommunities(page, [COMMUNITY_A], COMMUNITY_A.id);
-2
View File
@@ -47,8 +47,6 @@ type MockManagedAgentSeed = {
name: string;
personaId?: string | null;
status?: "running" | "stopped" | "deployed" | "not_deployed";
/** Home-relay pin; defaults to the mock workspace relay when omitted. */
relayUrl?: string;
channelNames?: string[];
channelIds?: string[];
backend?: