diff --git a/packages/agent-graph/src/canvas/draw-agents.ts b/packages/agent-graph/src/canvas/draw-agents.ts index 8239532f..22400d0d 100644 --- a/packages/agent-graph/src/canvas/draw-agents.ts +++ b/packages/agent-graph/src/canvas/draw-agents.ts @@ -50,6 +50,17 @@ export function drawAgents( // Breathing animation + spawn/waiting effects drawBreathing(ctx, x, y, r, node.state, time, node.spawnStatus); + // Working indicator: subtle spinning arc when member has active task + if (node.currentTaskId && (node.state === 'active' || node.state === 'thinking' || node.state === 'tool_calling')) { + const ringR = r + 4; + const rotation = time * 1.5; + ctx.beginPath(); + ctx.arc(x, y, ringR, rotation, rotation + Math.PI * 0.8); + ctx.strokeStyle = hexWithAlpha(color, 0.4); + ctx.lineWidth = 1.5; + ctx.stroke(); + } + // Name + role label (single line: "jack · developer") const labelText = node.role ? `${node.label} · ${node.role}` : node.label; drawLabel(ctx, x, y, r, labelText, color); diff --git a/packages/agent-graph/src/ports/types.ts b/packages/agent-graph/src/ports/types.ts index 4bafef3b..3b50c13e 100644 --- a/packages/agent-graph/src/ports/types.ts +++ b/packages/agent-graph/src/ports/types.ts @@ -49,6 +49,10 @@ export interface GraphNode { spawnStatus?: 'offline' | 'waiting' | 'spawning' | 'online' | 'error'; /** Context window usage ratio (0..1), available for lead only */ contextUsage?: number; + /** Current task ID this member is working on */ + currentTaskId?: string | null; + /** Current task subject (for display in popover) */ + currentTaskSubject?: string; // ─── Task-specific ───────────────────────────────────────────────────── /** Short display ID (e.g., "#3") */ diff --git a/src/renderer/features/agent-graph/adapters/TeamGraphAdapter.ts b/src/renderer/features/agent-graph/adapters/TeamGraphAdapter.ts index 71b3cd40..4f319801 100644 --- a/src/renderer/features/agent-graph/adapters/TeamGraphAdapter.ts +++ b/src/renderer/features/agent-graph/adapters/TeamGraphAdapter.ts @@ -158,6 +158,10 @@ export class TeamGraphAdapter { role: member.role ?? undefined, spawnStatus: spawn?.status, avatarUrl: agentAvatarUrl(member.name, 64), + currentTaskId: member.currentTaskId ?? undefined, + currentTaskSubject: member.currentTaskId + ? data.tasks.find((t) => t.id === member.currentTaskId)?.subject + : undefined, domainRef: { kind: 'member', teamName, memberName: member.name }, }); diff --git a/src/renderer/features/agent-graph/ui/GraphNodePopover.tsx b/src/renderer/features/agent-graph/ui/GraphNodePopover.tsx index 0b124a95..520ccfd3 100644 --- a/src/renderer/features/agent-graph/ui/GraphNodePopover.tsx +++ b/src/renderer/features/agent-graph/ui/GraphNodePopover.tsx @@ -7,7 +7,7 @@ import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; import { agentAvatarUrl } from '@renderer/utils/memberHelpers'; -import { MessageSquare, ExternalLink, User, Plus } from 'lucide-react'; +import { Loader2, MessageSquare, ExternalLink, User, Plus } from 'lucide-react'; import type { GraphNode } from '@claude-teams/agent-graph'; @@ -36,6 +36,7 @@ export function GraphNodePopover({ onSendMessage={onSendMessage} onOpenProfile={onOpenMemberProfile} onCreateTask={onCreateTask} + onOpenTask={onOpenTaskDetail} /> ); } @@ -70,12 +71,14 @@ function MemberPopoverContent({ onSendMessage, onOpenProfile, onCreateTask, + onOpenTask, }: { node: GraphNode; onClose: () => void; onSendMessage?: (name: string) => void; onOpenProfile?: (name: string) => void; onCreateTask?: (owner: string) => void; + onOpenTask?: (taskId: string) => void; }): React.JSX.Element { const memberName = node.domainRef.kind === 'member' ? node.domainRef.memberName : 'team-lead'; const avatarSrc = node.avatarUrl ?? agentAvatarUrl(memberName, 64); @@ -171,6 +174,31 @@ function MemberPopoverContent({ )} + {/* Current task indicator — reuses same pattern as MemberCard */} + {node.currentTaskId && node.currentTaskSubject && ( +
+ + working on + +
+ )} + {/* Actions */}