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:
Brainy Bumble
2026-08-14 11:26:40 -05:00
co-authored by Atish Patel
parent 17d2147eca
commit b87c30f8c4
5 changed files with 215 additions and 37 deletions
@@ -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);
});
}
+29
View File
@@ -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,
}) => {