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
|
// Breathing animation + spawn/waiting effects
|
||||||
drawBreathing(ctx, x, y, r, node.state, time, node.spawnStatus);
|
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")
|
// Name + role label (single line: "jack · developer")
|
||||||
const labelText = node.role ? `${node.label} · ${node.role}` : node.label;
|
const labelText = node.role ? `${node.label} · ${node.role}` : node.label;
|
||||||
drawLabel(ctx, x, y, r, labelText, color);
|
drawLabel(ctx, x, y, r, labelText, color);
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,10 @@ export interface GraphNode {
|
||||||
spawnStatus?: 'offline' | 'waiting' | 'spawning' | 'online' | 'error';
|
spawnStatus?: 'offline' | 'waiting' | 'spawning' | 'online' | 'error';
|
||||||
/** Context window usage ratio (0..1), available for lead only */
|
/** Context window usage ratio (0..1), available for lead only */
|
||||||
contextUsage?: number;
|
contextUsage?: number;
|
||||||
|
/** Current task ID this member is working on */
|
||||||
|
currentTaskId?: string | null;
|
||||||
|
/** Current task subject (for display in popover) */
|
||||||
|
currentTaskSubject?: string;
|
||||||
|
|
||||||
// ─── Task-specific ─────────────────────────────────────────────────────
|
// ─── Task-specific ─────────────────────────────────────────────────────
|
||||||
/** Short display ID (e.g., "#3") */
|
/** Short display ID (e.g., "#3") */
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,10 @@ export class TeamGraphAdapter {
|
||||||
role: member.role ?? undefined,
|
role: member.role ?? undefined,
|
||||||
spawnStatus: spawn?.status,
|
spawnStatus: spawn?.status,
|
||||||
avatarUrl: agentAvatarUrl(member.name, 64),
|
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 },
|
domainRef: { kind: 'member', teamName, memberName: member.name },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
import { Badge } from '@renderer/components/ui/badge';
|
import { Badge } from '@renderer/components/ui/badge';
|
||||||
import { Button } from '@renderer/components/ui/button';
|
import { Button } from '@renderer/components/ui/button';
|
||||||
import { agentAvatarUrl } from '@renderer/utils/memberHelpers';
|
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';
|
import type { GraphNode } from '@claude-teams/agent-graph';
|
||||||
|
|
||||||
|
|
@ -36,6 +36,7 @@ export function GraphNodePopover({
|
||||||
onSendMessage={onSendMessage}
|
onSendMessage={onSendMessage}
|
||||||
onOpenProfile={onOpenMemberProfile}
|
onOpenProfile={onOpenMemberProfile}
|
||||||
onCreateTask={onCreateTask}
|
onCreateTask={onCreateTask}
|
||||||
|
onOpenTask={onOpenTaskDetail}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -70,12 +71,14 @@ function MemberPopoverContent({
|
||||||
onSendMessage,
|
onSendMessage,
|
||||||
onOpenProfile,
|
onOpenProfile,
|
||||||
onCreateTask,
|
onCreateTask,
|
||||||
|
onOpenTask,
|
||||||
}: {
|
}: {
|
||||||
node: GraphNode;
|
node: GraphNode;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSendMessage?: (name: string) => void;
|
onSendMessage?: (name: string) => void;
|
||||||
onOpenProfile?: (name: string) => void;
|
onOpenProfile?: (name: string) => void;
|
||||||
onCreateTask?: (owner: string) => void;
|
onCreateTask?: (owner: string) => void;
|
||||||
|
onOpenTask?: (taskId: string) => void;
|
||||||
}): React.JSX.Element {
|
}): React.JSX.Element {
|
||||||
const memberName = node.domainRef.kind === 'member' ? node.domainRef.memberName : 'team-lead';
|
const memberName = node.domainRef.kind === 'member' ? node.domainRef.memberName : 'team-lead';
|
||||||
const avatarSrc = node.avatarUrl ?? agentAvatarUrl(memberName, 64);
|
const avatarSrc = node.avatarUrl ?? agentAvatarUrl(memberName, 64);
|
||||||
|
|
@ -171,6 +174,31 @@ function MemberPopoverContent({
|
||||||
</div>
|
</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 */}
|
{/* Actions */}
|
||||||
<div className="mt-3 flex flex-wrap gap-1.5">
|
<div className="mt-3 flex flex-wrap gap-1.5">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue