mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(mesh): CTA reads resolved provider; drop the row chip
The call to action never disappeared after setting up a mesh agent because it asked AgentPersona.provider — the raw definition field. The mesh provider is normally set at the global layer (global-agent-config.json), so that field is null for most mesh agents and the check answered "no" for all of them. Rust already resolves the layering (definition -> global for linked instances, instance -> global for definition-less ones) in resolve_effective_config, and runtime.rs builds the ManagedAgent DTO from the resolved value. Detection now consumes that field and holds no precedence rules of its own, rather than hand-rolling a second copy of the layering that would drift from Rust's. The CTA is now a tip with a pulsing dot rather than a bare link, and it still appears when the community has capacity even if this machine is not sharing — using someone else's compute is a legitimate reason to set up an agent. Removes the ManagedAgentRow shared-compute chip. Buzz already spends per-message and per-row provenance on ownership (MessageAgentOwner: "managed by X"), which answers a governance question; a second provenance marker for a config fact competes with it for the same space. Signed-off-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
@@ -5,14 +5,12 @@ import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
RefreshCw,
|
||||
Share2,
|
||||
} from "lucide-react";
|
||||
|
||||
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
|
||||
import { PresenceDot } from "@/features/presence/ui/PresenceBadge";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { AgentStatusBadge } from "@/features/agents/ui/AgentStatusBadge";
|
||||
import { usesMeshCompute } from "@/features/mesh-compute/meshAgents";
|
||||
import { useAgentWorking } from "@/features/agents/agentWorkingSignal";
|
||||
import { useOpenAgentActivity } from "@/features/agents/useOpenAgentActivity";
|
||||
import { formatElapsed } from "@/features/agents/ui/agentSessionUtils";
|
||||
@@ -409,23 +407,8 @@ function RuntimeBlock({
|
||||
<p className="truncate font-mono text-xs text-foreground">
|
||||
{agent.agentCommand}
|
||||
</p>
|
||||
{runtimeSource || agent.model || usesMeshCompute(agent) ? (
|
||||
{runtimeSource || agent.model ? (
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
||||
{/*
|
||||
Where an agent's inference runs is otherwise invisible without
|
||||
opening its config, and "on shared compute" is the one provider
|
||||
whose health the user can actually act on from the sidebar card.
|
||||
*/}
|
||||
{usesMeshCompute(agent) ? (
|
||||
<span
|
||||
className="inline-flex items-center gap-1 text-emerald-600 dark:text-emerald-400"
|
||||
data-testid="agent-uses-mesh"
|
||||
title="Runs on Buzz shared compute"
|
||||
>
|
||||
<Share2 aria-hidden className="h-3 w-3" />
|
||||
Shared compute
|
||||
</span>
|
||||
) : null}
|
||||
{runtimeSource ? <span>{runtimeSource}</span> : null}
|
||||
{agent.model ? <span>{agent.model}</span> : null}
|
||||
</div>
|
||||
|
||||
@@ -4,68 +4,45 @@ import test from "node:test";
|
||||
/**
|
||||
* Contract tests for mesh agent detection and the card's call to action.
|
||||
*
|
||||
* The CTA's value is that it stays quiet. A nudge that renders every time the
|
||||
* card does becomes furniture, so most of these tests pin *silence*.
|
||||
* Two things matter here. First, detection must read the **resolved** provider
|
||||
* off `ManagedAgent`: the mesh provider is usually set at the global layer, so
|
||||
* a persona's own `provider` field is null for most mesh agents — asking the
|
||||
* persona was the original bug. Second, the CTA's value is that it stays
|
||||
* quiet, so most of these tests pin *silence*.
|
||||
*/
|
||||
|
||||
import {
|
||||
deriveMeshCallToAction,
|
||||
summarizeMeshAgents,
|
||||
usesMeshCompute,
|
||||
} from "./meshAgents.ts";
|
||||
import { deriveMeshCallToAction, usesMeshCompute } from "./meshAgents.ts";
|
||||
|
||||
function persona(overrides = {}) {
|
||||
return {
|
||||
id: "p1",
|
||||
displayName: "Hop",
|
||||
provider: "relay-mesh",
|
||||
...overrides,
|
||||
};
|
||||
function agent(overrides = {}) {
|
||||
return { pubkey: "a1", provider: "relay-mesh", ...overrides };
|
||||
}
|
||||
|
||||
test("mesh agents are identified by provider, not by model string", () => {
|
||||
const summary = summarizeMeshAgents([
|
||||
persona({ id: "a", displayName: "Zoe" }),
|
||||
persona({ id: "b", displayName: "Ada" }),
|
||||
persona({ id: "c", displayName: "Other", provider: "anthropic" }),
|
||||
// A model that merely mentions a mesh model is not a mesh agent: intent
|
||||
// lives in the persisted provider, not in a free-text model field.
|
||||
persona({ id: "d", displayName: "Lookalike", provider: null }),
|
||||
]);
|
||||
assert.equal(summary.count, 2);
|
||||
assert.equal(summary.hasAny, true);
|
||||
assert.deepEqual(summary.names, ["Ada", "Zoe"], "names sort for stable copy");
|
||||
});
|
||||
|
||||
test("provider matching tolerates whitespace and absence", () => {
|
||||
assert.equal(usesMeshCompute(persona({ provider: " relay-mesh " })), true);
|
||||
assert.equal(usesMeshCompute(persona({ provider: "anthropic" })), false);
|
||||
assert.equal(usesMeshCompute(persona({ provider: null })), false);
|
||||
test("detection reads the resolved provider, whitespace tolerant", () => {
|
||||
assert.equal(usesMeshCompute(agent()), true);
|
||||
assert.equal(usesMeshCompute(agent({ provider: " relay-mesh " })), true);
|
||||
assert.equal(usesMeshCompute(agent({ provider: "anthropic" })), false);
|
||||
// Null is the common case for an agent inheriting a NON-mesh global default,
|
||||
// and for an orphaned instance. Neither is a mesh agent.
|
||||
assert.equal(usesMeshCompute(agent({ provider: null })), false);
|
||||
assert.equal(usesMeshCompute(null), false);
|
||||
assert.equal(usesMeshCompute(undefined), false);
|
||||
});
|
||||
|
||||
test("an empty persona list is not an error", () => {
|
||||
const summary = summarizeMeshAgents([]);
|
||||
assert.equal(summary.hasAny, false);
|
||||
assert.deepEqual(summary.names, []);
|
||||
assert.equal(summarizeMeshAgents(undefined).hasAny, false);
|
||||
});
|
||||
|
||||
test("the CTA appears only when compute has no consumer", () => {
|
||||
// Sharing, but nothing can use it — the dead end worth naming.
|
||||
test("the CTA appears when there is compute and nothing using it", () => {
|
||||
// Sharing from this machine.
|
||||
assert.deepEqual(
|
||||
deriveMeshCallToAction({
|
||||
personas: [persona({ provider: "anthropic" })],
|
||||
agents: [agent({ provider: "anthropic" })],
|
||||
isSharing: true,
|
||||
meshHasCapacity: false,
|
||||
}),
|
||||
{ kind: "createAgent", label: "Set up an agent to use it" },
|
||||
);
|
||||
// Not sharing, but the community has capacity this person could use.
|
||||
// Not sharing, but the community has capacity this person could use — a
|
||||
// legitimate reason to set up an agent even without contributing.
|
||||
assert.equal(
|
||||
deriveMeshCallToAction({
|
||||
personas: [],
|
||||
agents: [],
|
||||
isSharing: false,
|
||||
meshHasCapacity: true,
|
||||
}).kind,
|
||||
@@ -73,10 +50,10 @@ test("the CTA appears only when compute has no consumer", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("the CTA stays silent once a mesh agent exists", () => {
|
||||
test("the CTA disappears the moment a mesh agent exists", () => {
|
||||
assert.deepEqual(
|
||||
deriveMeshCallToAction({
|
||||
personas: [persona()],
|
||||
agents: [agent({ provider: "anthropic" }), agent({ pubkey: "a2" })],
|
||||
isSharing: true,
|
||||
meshHasCapacity: true,
|
||||
}),
|
||||
@@ -85,11 +62,9 @@ test("the CTA stays silent once a mesh agent exists", () => {
|
||||
});
|
||||
|
||||
test("no capacity anywhere means the CTA would just move the dead end", () => {
|
||||
// Telling someone to build an agent for a mesh with nothing in it does not
|
||||
// help them; it relocates the problem.
|
||||
assert.deepEqual(
|
||||
deriveMeshCallToAction({
|
||||
personas: [],
|
||||
agents: [],
|
||||
isSharing: false,
|
||||
meshHasCapacity: false,
|
||||
}),
|
||||
@@ -97,12 +72,12 @@ test("no capacity anywhere means the CTA would just move the dead end", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("an unloaded persona list never prompts a possible duplicate", () => {
|
||||
// `undefined` is "not fetched yet". Prompting someone to create an agent they
|
||||
// may already have is worse than staying quiet for a moment.
|
||||
test("an unloaded agent list never prompts a possible duplicate", () => {
|
||||
// `undefined` is "not fetched yet". Prompting someone to create an agent
|
||||
// they may already have is worse than staying quiet for a moment.
|
||||
assert.deepEqual(
|
||||
deriveMeshCallToAction({
|
||||
personas: undefined,
|
||||
agents: undefined,
|
||||
isSharing: true,
|
||||
meshHasCapacity: true,
|
||||
}),
|
||||
|
||||
@@ -1,88 +1,66 @@
|
||||
import type { AgentPersona } from "@/shared/api/types";
|
||||
import type { ManagedAgent } from "@/shared/api/types";
|
||||
|
||||
/**
|
||||
* Which agents are set up to run on Buzz shared compute.
|
||||
*
|
||||
* A persona points at the mesh by carrying `provider === "relay-mesh"` — the
|
||||
* same id the backend gates mesh model discovery on
|
||||
* (`managed_agents::RELAY_MESH_PROVIDER_ID`), so this asks the persistent
|
||||
* record rather than inferring intent from a model string.
|
||||
* ## Why this reads `ManagedAgent`, not `AgentPersona`
|
||||
*
|
||||
* The point is a call to action: sharing compute that no agent can use is a
|
||||
* dead end, and the card is where someone would notice. So the card needs to
|
||||
* know not just *whether* mesh agents exist but whether the whole setup is
|
||||
* inert — capacity with no consumer, or a consumer with no capacity.
|
||||
* The mesh provider is usually set at the **global** layer
|
||||
* (`global-agent-config.json`), not per agent — a persona's own `provider`
|
||||
* field is null in that common case, so asking a persona "are you a mesh
|
||||
* agent?" answers "no" for every agent on a machine whose global default is
|
||||
* `relay-mesh`. That was the original bug here.
|
||||
*
|
||||
* Rust already resolves the layering (definition → global for linked
|
||||
* instances, instance → global for definition-less ones) in
|
||||
* `effective_config::resolve_effective_config`, and `runtime.rs` builds the
|
||||
* `ManagedAgent` DTO from that resolved value. So this file consumes the
|
||||
* resolved field and deliberately holds **no precedence rules of its own** —
|
||||
* a second copy of that layering would drift from the one in Rust.
|
||||
*/
|
||||
|
||||
/** The provider id a persona carries when it runs on shared compute. */
|
||||
/** The provider id an agent carries when it runs on shared compute. */
|
||||
export const RELAY_MESH_PROVIDER_ID = "relay-mesh";
|
||||
|
||||
export type MeshAgentSummary = {
|
||||
/** Agents configured to run on shared compute. */
|
||||
count: number;
|
||||
/** Display names, for naming one specifically. Sorted for stable copy. */
|
||||
names: string[];
|
||||
/** True when at least one such agent exists. */
|
||||
hasAny: boolean;
|
||||
};
|
||||
|
||||
export function summarizeMeshAgents(
|
||||
personas: AgentPersona[] | undefined,
|
||||
): MeshAgentSummary {
|
||||
const matching = (personas ?? []).filter(
|
||||
(persona) => persona.provider?.trim() === RELAY_MESH_PROVIDER_ID,
|
||||
);
|
||||
const names = matching
|
||||
.map((persona) => persona.displayName)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
return { count: matching.length, names, hasAny: matching.length > 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a given persona runs on shared compute.
|
||||
* Whether an agent runs on shared compute.
|
||||
*
|
||||
* Used to decorate an agent wherever it appears, so "this one is running on the
|
||||
* mesh" is legible without opening its config.
|
||||
* Takes the resolved `provider` off a `ManagedAgent`; never re-derives it.
|
||||
*/
|
||||
export function usesMeshCompute(
|
||||
persona: Pick<AgentPersona, "provider"> | null | undefined,
|
||||
agent: Pick<ManagedAgent, "provider"> | null | undefined,
|
||||
): boolean {
|
||||
return persona?.provider?.trim() === RELAY_MESH_PROVIDER_ID;
|
||||
return agent?.provider?.trim() === RELAY_MESH_PROVIDER_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* The card's call to action, or null when none is warranted.
|
||||
* The card's call to action, or `none` when none is warranted.
|
||||
*
|
||||
* Deliberately silent in the common case. A nudge that appears every time the
|
||||
* card renders becomes furniture, so this only speaks when the setup is
|
||||
* genuinely incomplete in a way the person can act on:
|
||||
*
|
||||
* - sharing (or capacity exists) but no agent can use it → offer to make one
|
||||
* - an agent exists but nothing is shared anywhere → offer to share
|
||||
*
|
||||
* Returns null while the picture is still unknown (`personas === undefined`),
|
||||
* because prompting someone to create an agent they may already have is worse
|
||||
* than staying quiet for a moment.
|
||||
* genuinely incomplete in a way the person can act on: there is compute to use
|
||||
* and nothing set up to use it.
|
||||
*/
|
||||
export type MeshCallToAction =
|
||||
| { kind: "createAgent"; label: string }
|
||||
| { kind: "none" };
|
||||
|
||||
export function deriveMeshCallToAction({
|
||||
personas,
|
||||
agents,
|
||||
isSharing,
|
||||
meshHasCapacity,
|
||||
}: {
|
||||
personas: AgentPersona[] | undefined;
|
||||
/** Resolved agent list. `undefined` while still loading. */
|
||||
agents: ManagedAgent[] | undefined;
|
||||
isSharing: boolean;
|
||||
/** Anyone in the community is sharing, including this machine. */
|
||||
meshHasCapacity: boolean;
|
||||
}): MeshCallToAction {
|
||||
// Unknown yet — say nothing rather than nudge toward a duplicate.
|
||||
if (personas === undefined) {
|
||||
if (agents === undefined) {
|
||||
return { kind: "none" };
|
||||
}
|
||||
if (summarizeMeshAgents(personas).hasAny) {
|
||||
if (agents.some(usesMeshCompute)) {
|
||||
return { kind: "none" };
|
||||
}
|
||||
// Compute with no consumer is the dead end worth naming. Only prompt when
|
||||
|
||||
@@ -8,7 +8,7 @@ import { meshStartNode, meshStopNode } from "@/shared/api/tauriMesh";
|
||||
import type { MeshModelCatalog } from "@/shared/api/tauriMesh";
|
||||
import { meshModelCatalog } from "@/shared/api/tauriMesh";
|
||||
|
||||
import { usePersonasQuery } from "@/features/agents/hooks";
|
||||
import { useManagedAgentsQuery } from "@/features/agents/hooks";
|
||||
import { requestOpenCreateAgent } from "@/features/agents/openCreateAgentEvent";
|
||||
import { deriveMeshCallToAction } from "../meshAgents";
|
||||
import { useMeshDownloadProgress } from "../hooks/useMeshDownloadProgress";
|
||||
@@ -103,11 +103,12 @@ export function SidebarMeshComputeCard({
|
||||
useMeshDownloadProgress();
|
||||
|
||||
// Sharing compute no agent can use is a dead end, and this card is where
|
||||
// someone would notice. The CTA stays silent unless the setup is actually
|
||||
// incomplete — see deriveMeshCallToAction.
|
||||
const { data: personas } = usePersonasQuery();
|
||||
// someone would notice. Reads the resolved `provider` off ManagedAgent, not
|
||||
// a persona's raw field: the mesh provider is usually set globally, so the
|
||||
// persona value is null for most mesh agents. See meshAgents.ts.
|
||||
const { data: managedAgents } = useManagedAgentsQuery();
|
||||
const callToAction = deriveMeshCallToAction({
|
||||
personas,
|
||||
agents: managedAgents,
|
||||
isSharing: toggle.isSharing,
|
||||
meshHasCapacity: (snapshot?.sharingDeviceCount ?? 0) > 0,
|
||||
});
|
||||
@@ -275,13 +276,22 @@ export function SidebarMeshComputeCard({
|
||||
/>
|
||||
|
||||
{callToAction.kind === "createAgent" ? (
|
||||
// A tip, not a warning: there is compute here and nothing set up to
|
||||
// use it. The dot pulses to catch the eye once, then the tip simply
|
||||
// disappears the moment a mesh agent exists.
|
||||
<button
|
||||
className="mt-2 text-2xs font-medium text-foreground/80 underline-offset-2 transition-colors hover:text-foreground hover:underline focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
|
||||
className="mt-2 flex w-full items-center gap-1.5 rounded-md border border-border/60 bg-background/60 px-2 py-1.5 text-left text-2xs font-medium text-foreground/80 transition-colors hover:border-border hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
|
||||
data-testid="mesh-card-create-agent"
|
||||
onClick={() => requestOpenCreateAgent()}
|
||||
type="button"
|
||||
>
|
||||
{callToAction.label}
|
||||
<span className="relative flex h-1.5 w-1.5 shrink-0">
|
||||
{shouldReduceMotion ? null : (
|
||||
<span className="absolute h-1.5 w-1.5 animate-ping rounded-full bg-amber-500 dark:bg-amber-400" />
|
||||
)}
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-amber-500 dark:bg-amber-400" />
|
||||
</span>
|
||||
<span className="min-w-0 truncate">{callToAction.label}</span>
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user