feat(graph): current task indicator + working spinner on member nodes
Popover: - Shows "working on [task subject]" with spinning Loader2 when member has currentTaskId (same pattern as CurrentTaskIndicator in MemberCard) - Click task subject → opens TaskDetailDialog Canvas: - Active members with currentTaskId get subtle spinning arc around hexagon - Spinner only shows when state is active/thinking/tool_calling Adapter: - Passes currentTaskId + currentTaskSubject from ResolvedTeamMember - Looks up task subject from data.tasks Port types: - Added currentTaskId, currentTaskSubject to GraphNode
This commit is contained in:
parent
add9df2006
commit
ee5b7b5888
4 changed files with 48 additions and 1 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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") */
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Current task indicator — reuses same pattern as MemberCard */}
|
||||
{node.currentTaskId && node.currentTaskSubject && (
|
||||
<div className="mt-2 flex items-center gap-1.5 text-[10px]">
|
||||
<Loader2
|
||||
className="size-3 shrink-0 animate-spin"
|
||||
style={{ color: node.color ?? '#66ccff' }}
|
||||
/>
|
||||
<span className="shrink-0 text-[var(--color-text-muted)]">working on</span>
|
||||
<button
|
||||
type="button"
|
||||
className="min-w-0 truncate rounded px-1.5 py-0.5 font-medium text-[var(--color-text)] transition-opacity hover:opacity-90"
|
||||
style={{ border: `1px solid ${node.color ?? '#66ccff'}40` }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpenTask?.(node.currentTaskId!);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{node.currentTaskSubject.length > 30
|
||||
? `${node.currentTaskSubject.slice(0, 30)}…`
|
||||
: node.currentTaskSubject}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="mt-3 flex flex-wrap gap-1.5">
|
||||
<Button
|
||||
|
|
|
|||
Loading…
Reference in a new issue