mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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:
co-authored by
Claude Fable 5
parent
1712cbd2ed
commit
87e5b6d172
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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?:
|
||||
|
||||
Reference in New Issue
Block a user