mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): show every managed agent instance
Signed-off-by: Brainy Bumble <0ed7657b57c0e8a9f5288390dd6c8d5d0a3a06abe9b01b9006814f52077d6cdf@buzz.block.builderlab.xyz> Co-authored-by: Atish Patel <atish@squareup.com> Signed-off-by: Brainy Bumble <0ed7657b57c0e8a9f5288390dd6c8d5d0a3a06abe9b01b9006814f52077d6cdf@buzz.block.builderlab.xyz> Signed-off-by: Atish Patel <atish@squareup.com>
This commit is contained in:
co-authored by
Atish Patel
parent
17d2147eca
commit
b87c30f8c4
@@ -11,6 +11,7 @@ type AgentIdentityCardProps = {
|
||||
avatarUrl?: string | null;
|
||||
dataTestId: string;
|
||||
label: string;
|
||||
identityLabel?: string | null;
|
||||
modelLabel?: string | null;
|
||||
onClick: () => void;
|
||||
/** Optional badge rendered below the label (e.g. "Restart required"). */
|
||||
@@ -24,6 +25,7 @@ export function AgentIdentityCard({
|
||||
avatarUrl,
|
||||
dataTestId,
|
||||
label,
|
||||
identityLabel,
|
||||
modelLabel,
|
||||
onClick,
|
||||
statusBadge,
|
||||
@@ -72,6 +74,11 @@ export function AgentIdentityCard({
|
||||
<span className="min-w-0 truncate font-semibold text-foreground tracking-normal">
|
||||
{label}
|
||||
</span>
|
||||
{identityLabel ? (
|
||||
<span className="min-w-0 truncate font-mono text-2xs font-normal text-secondary-foreground/75">
|
||||
{identityLabel}
|
||||
</span>
|
||||
) : null}
|
||||
{modelLabel ? (
|
||||
<span className="min-w-0 truncate text-xs font-normal text-secondary-foreground/75">
|
||||
{modelLabel}
|
||||
|
||||
@@ -8,18 +8,21 @@ import {
|
||||
import { resolveAgentCardModelLabel } from "@/features/agents/lib/agentCardModelLabel";
|
||||
import { friendlyAgentLastError } from "@/features/agents/lib/friendlyAgentLastError";
|
||||
import { isManagedAgentActive } from "@/features/agents/lib/managedAgentControlActions";
|
||||
import { pickProfileAgent } from "@/features/agents/lib/pickProfileAgent";
|
||||
import { useUserProfileQuery } from "@/features/profile/hooks";
|
||||
import type { AgentPersona, ManagedAgent } from "@/shared/api/types";
|
||||
import type { ProfilePanelOpenOptions } from "@/shared/context/ProfilePanelContext";
|
||||
import { useFeedbackToasts } from "@/shared/hooks/useToastEffect";
|
||||
import { truncatePubkey } from "@/shared/lib/pubkey";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { IdentityCardSkeleton } from "@/shared/ui/identity-card-skeleton";
|
||||
import { AgentIdentityCard } from "./AgentIdentityCard";
|
||||
import { AgentRuntimeAvatarControl } from "./AgentRuntimeAvatarControl";
|
||||
import { CreateIdentityCard } from "./CreateIdentityCard";
|
||||
import { PersonaActionsMenu } from "./PersonaActionsMenu";
|
||||
import { buildUnifiedGroups } from "./unifiedAgentGroups";
|
||||
import {
|
||||
buildUnifiedGroups,
|
||||
profileAgentsForGroup,
|
||||
} from "./unifiedAgentGroups";
|
||||
|
||||
type UnifiedAgentsSectionProps = {
|
||||
defaultModel: string;
|
||||
@@ -128,41 +131,96 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) {
|
||||
disabled={isPersonasPending}
|
||||
onClick={onOpenCatalog}
|
||||
/>
|
||||
{groups.map((group) => {
|
||||
const profileAgent = pickProfileAgent(group.agents);
|
||||
return (
|
||||
<AgentPersonaCard
|
||||
actions={(effectiveAvatarUrl, isEffectiveAvatarLoading) => (
|
||||
<PersonaActionsMenu
|
||||
isActionPending={
|
||||
isActionPending || isEffectiveAvatarLoading
|
||||
}
|
||||
isPending={isPersonasPending}
|
||||
persona={group.persona}
|
||||
linkedAgent={profileAgent}
|
||||
onDeactivate={onDeactivatePersona}
|
||||
onDelete={onDeletePersona}
|
||||
onDuplicate={onDuplicatePersona}
|
||||
onEdit={onEditPersona}
|
||||
onShare={(persona, linkedAgent) =>
|
||||
onSharePersona(persona, linkedAgent, effectiveAvatarUrl)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
agent={profileAgent}
|
||||
defaultModel={defaultModel}
|
||||
key={group.persona.id}
|
||||
persona={group.persona}
|
||||
restartingAgentPubkey={restartingAgentPubkey}
|
||||
startingAgentPubkey={startingAgentPubkey}
|
||||
startingPersonaIds={startingPersonaIds}
|
||||
onOpenAgentProfile={onOpenAgentProfile}
|
||||
onOpenPersonaProfile={onOpenPersonaProfile}
|
||||
onRestartAgent={onRestartAgent}
|
||||
onStartAgent={onStartAgent}
|
||||
onStartPersona={onStartPersona}
|
||||
/>
|
||||
);
|
||||
{groups.flatMap((group) => {
|
||||
const profileAgents = profileAgentsForGroup(group.agents);
|
||||
const cards: Array<React.ReactElement> = [];
|
||||
|
||||
if (profileAgents.length === 0) {
|
||||
cards.push(
|
||||
<AgentPersonaCard
|
||||
actions={(effectiveAvatarUrl, isEffectiveAvatarLoading) => (
|
||||
<PersonaActionsMenu
|
||||
isActionPending={
|
||||
isActionPending || isEffectiveAvatarLoading
|
||||
}
|
||||
isPending={isPersonasPending}
|
||||
persona={group.persona}
|
||||
linkedAgent={undefined}
|
||||
onDeactivate={onDeactivatePersona}
|
||||
onDelete={onDeletePersona}
|
||||
onDuplicate={onDuplicatePersona}
|
||||
onEdit={onEditPersona}
|
||||
onShare={(persona, linkedAgent) =>
|
||||
onSharePersona(
|
||||
persona,
|
||||
linkedAgent,
|
||||
effectiveAvatarUrl,
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
agent={undefined}
|
||||
defaultModel={defaultModel}
|
||||
key={`persona:${group.persona.id}`}
|
||||
persona={group.persona}
|
||||
restartingAgentPubkey={restartingAgentPubkey}
|
||||
startingAgentPubkey={startingAgentPubkey}
|
||||
startingPersonaIds={startingPersonaIds}
|
||||
onOpenAgentProfile={onOpenAgentProfile}
|
||||
onOpenPersonaProfile={onOpenPersonaProfile}
|
||||
onRestartAgent={onRestartAgent}
|
||||
onStartAgent={onStartAgent}
|
||||
onStartPersona={onStartPersona}
|
||||
/>,
|
||||
);
|
||||
return cards;
|
||||
}
|
||||
|
||||
for (const [index, profileAgent] of profileAgents.entries()) {
|
||||
cards.push(
|
||||
<AgentPersonaCard
|
||||
actions={
|
||||
index === 0
|
||||
? (effectiveAvatarUrl, isEffectiveAvatarLoading) => (
|
||||
<PersonaActionsMenu
|
||||
isActionPending={
|
||||
isActionPending || isEffectiveAvatarLoading
|
||||
}
|
||||
isPending={isPersonasPending}
|
||||
persona={group.persona}
|
||||
linkedAgent={profileAgent}
|
||||
onDeactivate={onDeactivatePersona}
|
||||
onDelete={onDeletePersona}
|
||||
onDuplicate={onDuplicatePersona}
|
||||
onEdit={onEditPersona}
|
||||
onShare={(persona, linkedAgent) =>
|
||||
onSharePersona(
|
||||
persona,
|
||||
linkedAgent,
|
||||
effectiveAvatarUrl,
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
agent={profileAgent}
|
||||
defaultModel={defaultModel}
|
||||
key={`agent:${profileAgent.pubkey}`}
|
||||
persona={group.persona}
|
||||
restartingAgentPubkey={restartingAgentPubkey}
|
||||
startingAgentPubkey={startingAgentPubkey}
|
||||
startingPersonaIds={startingPersonaIds}
|
||||
onOpenAgentProfile={onOpenAgentProfile}
|
||||
onOpenPersonaProfile={onOpenPersonaProfile}
|
||||
onRestartAgent={onRestartAgent}
|
||||
onStartAgent={onStartAgent}
|
||||
onStartPersona={onStartPersona}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
return cards;
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -309,6 +367,7 @@ function AgentPersonaCard({
|
||||
}
|
||||
avatarUrl={avatarUrl}
|
||||
dataTestId={`persona-agent-row-${persona.id}`}
|
||||
identityLabel={agent ? truncatePubkey(agent.pubkey) : null}
|
||||
label={title}
|
||||
modelLabel={modelLabel}
|
||||
onClick={() => {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
buildUnifiedGroups,
|
||||
profileAgentsForGroup,
|
||||
} from "./unifiedAgentGroups.ts";
|
||||
|
||||
function agent(pubkey, overrides = {}) {
|
||||
return {
|
||||
pubkey,
|
||||
name: overrides.name ?? "Fizz",
|
||||
personaId: overrides.personaId ?? "builtin:fizz",
|
||||
status: overrides.status ?? "stopped",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const fizz = { id: "builtin:fizz", displayName: "Fizz" };
|
||||
|
||||
test("buildUnifiedGroups retains every managed instance for one persona", () => {
|
||||
const first = agent("a".repeat(64));
|
||||
const second = agent("b".repeat(64));
|
||||
|
||||
const { groups, ungrouped, unknown } = buildUnifiedGroups(
|
||||
[fizz],
|
||||
[first, second],
|
||||
);
|
||||
|
||||
assert.deepEqual(groups, [{ persona: fizz, agents: [first, second] }]);
|
||||
assert.deepEqual(ungrouped, []);
|
||||
assert.deepEqual(unknown, []);
|
||||
});
|
||||
|
||||
test("profileAgentsForGroup returns every instance in stable order without mutating input", () => {
|
||||
const stopped = agent("a".repeat(64), { name: "Zulu" });
|
||||
const runningLater = agent("c".repeat(64), {
|
||||
name: "Alpha",
|
||||
status: "running",
|
||||
});
|
||||
const runningEarlier = agent("b".repeat(64), {
|
||||
name: "Alpha",
|
||||
status: "running",
|
||||
});
|
||||
const input = [stopped, runningLater, runningEarlier];
|
||||
|
||||
assert.deepEqual(profileAgentsForGroup(input), [
|
||||
runningEarlier,
|
||||
runningLater,
|
||||
stopped,
|
||||
]);
|
||||
assert.deepEqual(input, [stopped, runningLater, runningEarlier]);
|
||||
});
|
||||
|
||||
test("a relay-restored persona instance follows the same visible group path", () => {
|
||||
const relayRestored = agent("c".repeat(64), {
|
||||
name: "Recovered Fizz",
|
||||
status: "stopped",
|
||||
});
|
||||
|
||||
const { groups } = buildUnifiedGroups([fizz], [relayRestored]);
|
||||
|
||||
assert.deepEqual(profileAgentsForGroup(groups[0].agents), [relayRestored]);
|
||||
});
|
||||
|
||||
test("a persona with no managed instance remains an empty group", () => {
|
||||
const { groups } = buildUnifiedGroups([fizz], []);
|
||||
|
||||
assert.deepEqual(groups, [{ persona: fizz, agents: [] }]);
|
||||
assert.deepEqual(profileAgentsForGroup(groups[0].agents), []);
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isManagedAgentActive } from "@/features/agents/lib/managedAgentControlActions";
|
||||
import type { AgentPersona, ManagedAgent } from "@/shared/api/types";
|
||||
|
||||
type PersonaGroup = { persona: AgentPersona; agents: ManagedAgent[] };
|
||||
@@ -32,3 +33,14 @@ export function buildUnifiedGroups(
|
||||
|
||||
return { groups, ungrouped, unknown };
|
||||
}
|
||||
|
||||
export function profileAgentsForGroup(agents: ManagedAgent[]) {
|
||||
return [...agents].sort((left, right) => {
|
||||
const activeDiff =
|
||||
Number(isManagedAgentActive(right)) - Number(isManagedAgentActive(left));
|
||||
if (activeDiff !== 0) return activeDiff;
|
||||
const nameDiff = left.name.localeCompare(right.name);
|
||||
if (nameDiff !== 0) return nameDiff;
|
||||
return left.pubkey.localeCompare(right.pubkey);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -226,6 +226,35 @@ async function countCommandInvocations(
|
||||
);
|
||||
}
|
||||
|
||||
test("Agents view keeps every managed instance that shares a persona visible", async ({
|
||||
page,
|
||||
}) => {
|
||||
const personaId = "custom:multi-fizz";
|
||||
const firstPubkey = "a".repeat(64);
|
||||
const secondPubkey = "b".repeat(64);
|
||||
await installMockBridge(page, {
|
||||
personas: [
|
||||
{
|
||||
id: personaId,
|
||||
displayName: "Fizz",
|
||||
systemPrompt: "Move fast.",
|
||||
},
|
||||
],
|
||||
managedAgents: [
|
||||
{ pubkey: firstPubkey, name: "Fizz", personaId },
|
||||
{ pubkey: secondPubkey, name: "Fizz", personaId },
|
||||
],
|
||||
});
|
||||
await gotoApp(page);
|
||||
await page.getByTestId("open-agents-view").click();
|
||||
|
||||
const cards = page.getByTestId(`persona-agent-row-${personaId}`);
|
||||
await expect(cards).toHaveCount(2);
|
||||
await expect(cards.nth(0)).toContainText("aaaaaaaa…aaaa");
|
||||
await expect(cards.nth(1)).toContainText("bbbbbbbb…bbbb");
|
||||
await expect(page.getByLabel("Open actions for Fizz")).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("catalog hides built-ins and shows the shared-agent empty state", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Reference in New Issue
Block a user