diff --git a/desktop/src/features/agents/activeAgentTurnsStore.test.mjs b/desktop/src/features/agents/activeAgentTurnsStore.test.mjs index cef73f602..9878abf8e 100644 --- a/desktop/src/features/agents/activeAgentTurnsStore.test.mjs +++ b/desktop/src/features/agents/activeAgentTurnsStore.test.mjs @@ -193,11 +193,18 @@ describe("activeAgentTurnsStore", () => { const summaries = getActiveTurnsByChannel(); assert.deepEqual( - summaries.map(({ channelId, agentCount }) => ({ + summaries.map(({ agentCount, agentPubkeys, channelId }) => ({ channelId, agentCount, + agentPubkeys, })), - [{ channelId: "shared", agentCount: 2 }], + [ + { + channelId: "shared", + agentCount: 2, + agentPubkeys: [AGENT, AGENT_2].sort(), + }, + ], ); assert.equal( summaries[0].anchorAt, diff --git a/desktop/src/features/agents/activeAgentTurnsStore.ts b/desktop/src/features/agents/activeAgentTurnsStore.ts index 6e04fc0b3..202054c22 100644 --- a/desktop/src/features/agents/activeAgentTurnsStore.ts +++ b/desktop/src/features/agents/activeAgentTurnsStore.ts @@ -49,6 +49,7 @@ export type ActiveChannelTurnSummary = { channelId: string; anchorAt: number; agentCount: number; + agentPubkeys: string[]; }; // Module-level state: agentPubkey → turnId → ActiveTurn @@ -474,11 +475,15 @@ export function getActiveTurnsByChannel(): ActiveChannelTurnSummary[] { } const result = [...summaries.entries()] - .map(([channelId, summary]) => ({ - channelId, - anchorAt: summary.anchorAt, - agentCount: summary.agentPubkeys.size, - })) + .map(([channelId, summary]) => { + const agentPubkeys = [...summary.agentPubkeys].sort(); + return { + channelId, + anchorAt: summary.anchorAt, + agentCount: agentPubkeys.length, + agentPubkeys, + }; + }) .sort((a, b) => a.channelId.localeCompare(b.channelId)); cachedChannelTurnSummaries = result; return result; diff --git a/desktop/src/features/sidebar/ui/SidebarSection.tsx b/desktop/src/features/sidebar/ui/SidebarSection.tsx index d435553b4..9d20b034d 100644 --- a/desktop/src/features/sidebar/ui/SidebarSection.tsx +++ b/desktop/src/features/sidebar/ui/SidebarSection.tsx @@ -18,6 +18,9 @@ import { import { ChannelContextMenuItems } from "@/features/sidebar/ui/CustomChannelSection"; import type { ActiveChannelTurnSummary } from "@/features/agents/activeAgentTurnsStore"; import { formatElapsed } from "@/features/agents/ui/agentSessionUtils"; +import { resolveUserLabel } from "@/features/profile/lib/identity"; +import { useUsersBatchQuery } from "@/features/profile/hooks"; +import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar"; import { getEphemeralChannelDisplay } from "@/features/channels/lib/ephemeralChannel"; import { EphemeralChannelBadge } from "@/features/channels/ui/EphemeralChannelBadge"; import { @@ -28,6 +31,7 @@ import { import type { Channel, PresenceStatus } from "@/shared/api/types"; import { cn } from "@/shared/lib/cn"; import { useNow } from "@/shared/lib/useNow"; +import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; import { SidebarGroup, SidebarGroupContent, @@ -99,7 +103,7 @@ function UnreadDotBadge({ ); } -function ChannelWorkingBadge({ +function ChannelWorkingAgents({ channelName, isActive, summary, @@ -110,24 +114,110 @@ function ChannelWorkingBadge({ }) { const now = useNow(1000); const elapsed = formatElapsed(now - summary.anchorAt); + const profilesQuery = useUsersBatchQuery(summary.agentPubkeys, { + enabled: summary.agentPubkeys.length > 0, + }); + const profiles = profilesQuery.data?.profiles; + const agents = summary.agentPubkeys.map((pubkey) => { + const profile = profiles?.[pubkey.toLowerCase()]; + const label = resolveUserLabel({ + pubkey, + profiles, + }); + + return { + avatarUrl: profile?.avatarUrl ?? null, + label, + pubkey, + }; + }); + const visibleAgents = agents.slice(0, 3); + const overflowCount = Math.max(0, agents.length - visibleAgents.length); const label = - summary.agentCount > 1 - ? `${summary.agentCount} working · ${elapsed}` - : `Working · ${elapsed}`; + agents.length > 1 + ? `${agents.length} agents working · ${elapsed}` + : `${agents[0]?.label ?? "Agent"} is working · ${elapsed}`; return ( - - {label} - + + + {/* biome-ignore lint/a11y/noStaticElementInteractions lint/a11y/useKeyWithClickEvents: Radix popover trigger sits inside the channel row button; click opens the popover without navigating the row. */} + { + event.stopPropagation(); + }} + onPointerDown={(event) => { + event.stopPropagation(); + }} + title={label} + > + + {visibleAgents.map((agent) => ( + + ))} + {overflowCount > 0 ? ( + + +{overflowCount} + + ) : null} + + + + event.stopPropagation()} + onOpenAutoFocus={(event) => event.preventDefault()} + side="right" + sideOffset={8} + > + + Working · {elapsed} + + + {agents.map((agent) => ( + + + {agent.label} + + + ))} + + + ); } @@ -283,7 +373,7 @@ export function ChannelMenuButton({ /> ) : null} {activeWorking ? ( -