mirror of
https://github.com/block/buzz.git
synced 2026-11-12 12:22:06 +00:00
feat(mesh): agent call-to-action and shared-compute decorator
Sharing compute that no agent can use is a dead end, and the sidebar card is where someone would notice it. The card now offers to create an agent — but only when the setup is genuinely incomplete: capacity exists (shared by this machine or the community) and no persona carries the relay-mesh provider. The CTA's value is that it stays quiet, so most of its tests pin silence. It says nothing once a mesh agent exists, nothing when there is no capacity anywhere (that would relocate the dead end rather than fix it), and nothing while the persona list is still loading, since prompting someone to create an agent they may already have is worse than a moment's quiet. Agents that run on shared compute are now decorated in ManagedAgentRow. Where an agent's inference runs is otherwise invisible without opening its config, and shared compute is the one provider whose health the owner can act on directly from the card. Detection reads the persisted provider id (relay-mesh, the same constant the backend gates mesh model discovery on) rather than inferring intent from a model string. Signed-off-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
@@ -5,12 +5,14 @@ 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";
|
||||
@@ -407,8 +409,23 @@ function RuntimeBlock({
|
||||
<p className="truncate font-mono text-xs text-foreground">
|
||||
{agent.agentCommand}
|
||||
</p>
|
||||
{runtimeSource || agent.model ? (
|
||||
{runtimeSource || agent.model || usesMeshCompute(agent) ? (
|
||||
<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>
|
||||
|
||||
111
desktop/src/features/mesh-compute/meshAgents.test.mjs
Normal file
111
desktop/src/features/mesh-compute/meshAgents.test.mjs
Normal file
@@ -0,0 +1,111 @@
|
||||
import assert from "node:assert/strict";
|
||||
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*.
|
||||
*/
|
||||
|
||||
import {
|
||||
deriveMeshCallToAction,
|
||||
summarizeMeshAgents,
|
||||
usesMeshCompute,
|
||||
} from "./meshAgents.ts";
|
||||
|
||||
function persona(overrides = {}) {
|
||||
return {
|
||||
id: "p1",
|
||||
displayName: "Hop",
|
||||
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);
|
||||
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.
|
||||
assert.deepEqual(
|
||||
deriveMeshCallToAction({
|
||||
personas: [persona({ 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.
|
||||
assert.equal(
|
||||
deriveMeshCallToAction({
|
||||
personas: [],
|
||||
isSharing: false,
|
||||
meshHasCapacity: true,
|
||||
}).kind,
|
||||
"createAgent",
|
||||
);
|
||||
});
|
||||
|
||||
test("the CTA stays silent once a mesh agent exists", () => {
|
||||
assert.deepEqual(
|
||||
deriveMeshCallToAction({
|
||||
personas: [persona()],
|
||||
isSharing: true,
|
||||
meshHasCapacity: true,
|
||||
}),
|
||||
{ kind: "none" },
|
||||
);
|
||||
});
|
||||
|
||||
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: [],
|
||||
isSharing: false,
|
||||
meshHasCapacity: false,
|
||||
}),
|
||||
{ kind: "none" },
|
||||
);
|
||||
});
|
||||
|
||||
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.
|
||||
assert.deepEqual(
|
||||
deriveMeshCallToAction({
|
||||
personas: undefined,
|
||||
isSharing: true,
|
||||
meshHasCapacity: true,
|
||||
}),
|
||||
{ kind: "none" },
|
||||
);
|
||||
});
|
||||
95
desktop/src/features/mesh-compute/meshAgents.ts
Normal file
95
desktop/src/features/mesh-compute/meshAgents.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { AgentPersona } 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.
|
||||
*
|
||||
* 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 provider id a persona 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.
|
||||
*
|
||||
* Used to decorate an agent wherever it appears, so "this one is running on the
|
||||
* mesh" is legible without opening its config.
|
||||
*/
|
||||
export function usesMeshCompute(
|
||||
persona: Pick<AgentPersona, "provider"> | null | undefined,
|
||||
): boolean {
|
||||
return persona?.provider?.trim() === RELAY_MESH_PROVIDER_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* The card's call to action, or null 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.
|
||||
*/
|
||||
export type MeshCallToAction =
|
||||
| { kind: "createAgent"; label: string }
|
||||
| { kind: "none" };
|
||||
|
||||
export function deriveMeshCallToAction({
|
||||
personas,
|
||||
isSharing,
|
||||
meshHasCapacity,
|
||||
}: {
|
||||
personas: AgentPersona[] | 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) {
|
||||
return { kind: "none" };
|
||||
}
|
||||
if (summarizeMeshAgents(personas).hasAny) {
|
||||
return { kind: "none" };
|
||||
}
|
||||
// Compute with no consumer is the dead end worth naming. Only prompt when
|
||||
// there is actually capacity to use — telling someone to build an agent for
|
||||
// a mesh with nothing in it just moves the dead end.
|
||||
if (isSharing || meshHasCapacity) {
|
||||
return { kind: "createAgent", label: "Set up an agent to use it" };
|
||||
}
|
||||
return { kind: "none" };
|
||||
}
|
||||
@@ -8,6 +8,9 @@ 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 { requestOpenCreateAgent } from "@/features/agents/openCreateAgentEvent";
|
||||
import { deriveMeshCallToAction } from "../meshAgents";
|
||||
import { useMeshDownloadProgress } from "../hooks/useMeshDownloadProgress";
|
||||
import { useMeshLiveView } from "../hooks/useMeshLiveView";
|
||||
import { useMeshNodeStatus } from "../hooks/useMeshNodeStatus";
|
||||
@@ -99,6 +102,16 @@ export function SidebarMeshComputeCard({
|
||||
const { progress: downloadProgress, reset: resetDownloadProgress } =
|
||||
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();
|
||||
const callToAction = deriveMeshCallToAction({
|
||||
personas,
|
||||
isSharing: toggle.isSharing,
|
||||
meshHasCapacity: (snapshot?.sharingDeviceCount ?? 0) > 0,
|
||||
});
|
||||
|
||||
// Inbound work has no counter in mesh-llm, so it is inferred by elimination
|
||||
// across two samples: serving, inflight > 0, and our own dispatch count flat
|
||||
// means the work is not ours. Keeping the previous sample in a ref (not
|
||||
@@ -261,6 +274,17 @@ export function SidebarMeshComputeCard({
|
||||
showSoloHint={model.showSoloHint}
|
||||
/>
|
||||
|
||||
{callToAction.kind === "createAgent" ? (
|
||||
<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"
|
||||
data-testid="mesh-card-create-agent"
|
||||
onClick={() => requestOpenCreateAgent()}
|
||||
type="button"
|
||||
>
|
||||
{callToAction.label}
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{errorText ? (
|
||||
<p
|
||||
className="mt-2 line-clamp-2 text-2xs leading-snug text-destructive"
|
||||
|
||||
Reference in New Issue
Block a user