feat(agent-graph): show pulsing amber ring on agents awaiting approval
- Add pendingApproval field to GraphNode type - Pass pendingApprovalAgents Set from store through adapter - Draw pulsing amber ring + subtle glow on agent nodes that have pending tool approval requests - Include approval state in adapter cache hash for reactivity
This commit is contained in:
parent
70b5f1962f
commit
e2000d0900
4 changed files with 60 additions and 7 deletions
|
|
@ -50,6 +50,27 @@ export function drawAgents(
|
|||
// Breathing animation + spawn/waiting effects
|
||||
drawBreathing(ctx, x, y, r, node.state, time, node.spawnStatus);
|
||||
|
||||
// Pending approval indicator: pulsing amber ring
|
||||
if (node.pendingApproval) {
|
||||
const pulseAlpha = 0.3 + 0.35 * Math.sin(time * 3);
|
||||
const ringR = r + 5;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, ringR, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = hexWithAlpha('#f59e0b', pulseAlpha);
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// Subtle amber glow
|
||||
const glowR = r + 12;
|
||||
const grad = ctx.createRadialGradient(x, y, r, x, y, glowR);
|
||||
grad.addColorStop(0, hexWithAlpha('#f59e0b', pulseAlpha * 0.25));
|
||||
grad.addColorStop(1, 'transparent');
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, glowR, 0, Math.PI * 2);
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ export interface GraphNode {
|
|||
currentTaskId?: string | null;
|
||||
/** Current task subject (for display in popover) */
|
||||
currentTaskSubject?: string;
|
||||
/** Agent is awaiting tool approval from the user */
|
||||
pendingApproval?: boolean;
|
||||
|
||||
// ─── Task-specific ─────────────────────────────────────────────────────
|
||||
/** Short display ID (e.g., "#3") */
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ export class TeamGraphAdapter {
|
|||
teamData: TeamData | null,
|
||||
teamName: string,
|
||||
spawnStatuses?: Record<string, MemberSpawnStatusEntry>,
|
||||
leadContext?: LeadContextUsage
|
||||
leadContext?: LeadContextUsage,
|
||||
pendingApprovalAgents?: Set<string>
|
||||
): GraphDataPort {
|
||||
if (teamData?.teamName !== teamName) {
|
||||
return TeamGraphAdapter.#emptyResult(teamName);
|
||||
|
|
@ -58,7 +59,10 @@ export class TeamGraphAdapter {
|
|||
|
||||
// Simple hash for change detection (avoids full deep equality)
|
||||
const totalComments = teamData.tasks.reduce((sum, t) => sum + (t.comments?.length ?? 0), 0);
|
||||
const hash = `${teamData.teamName}:${teamData.members.length}:${teamData.tasks.length}:${teamData.messages.length}:${teamData.isAlive}:${leadContext?.percent}:${totalComments}`;
|
||||
const approvalKey = pendingApprovalAgents?.size
|
||||
? Array.from(pendingApprovalAgents).sort().join(',')
|
||||
: '';
|
||||
const hash = `${teamData.teamName}:${teamData.members.length}:${teamData.tasks.length}:${teamData.messages.length}:${teamData.isAlive}:${leadContext?.percent}:${totalComments}:${approvalKey}`;
|
||||
if (hash === this.#lastDataHash && teamName === this.#lastTeamName) {
|
||||
return this.#cachedResult;
|
||||
}
|
||||
|
|
@ -82,7 +86,15 @@ export class TeamGraphAdapter {
|
|||
const leadId = `lead:${teamName}`;
|
||||
|
||||
this.#buildLeadNode(nodes, leadId, teamData, teamName, leadContext);
|
||||
this.#buildMemberNodes(nodes, edges, leadId, teamData, teamName, spawnStatuses);
|
||||
this.#buildMemberNodes(
|
||||
nodes,
|
||||
edges,
|
||||
leadId,
|
||||
teamData,
|
||||
teamName,
|
||||
spawnStatuses,
|
||||
pendingApprovalAgents
|
||||
);
|
||||
this.#buildTaskNodes(nodes, edges, teamData, teamName);
|
||||
this.#buildProcessNodes(nodes, edges, teamData, teamName);
|
||||
this.#buildMessageParticles(particles, teamData.messages, teamName, leadId, edges);
|
||||
|
|
@ -140,7 +152,8 @@ export class TeamGraphAdapter {
|
|||
leadId: string,
|
||||
data: TeamData,
|
||||
teamName: string,
|
||||
spawnStatuses?: Record<string, MemberSpawnStatusEntry>
|
||||
spawnStatuses?: Record<string, MemberSpawnStatusEntry>,
|
||||
pendingApprovalAgents?: Set<string>
|
||||
): void {
|
||||
for (const member of data.members) {
|
||||
if (member.removedAt) continue;
|
||||
|
|
@ -162,6 +175,7 @@ export class TeamGraphAdapter {
|
|||
currentTaskSubject: member.currentTaskId
|
||||
? data.tasks.find((t) => t.id === member.currentTaskId)?.subject
|
||||
: undefined,
|
||||
pendingApproval: pendingApprovalAgents?.has(member.name) ?? false,
|
||||
domainRef: { kind: 'member', teamName, memberName: member.name },
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -15,16 +15,32 @@ import type { GraphDataPort } from '@claude-teams/agent-graph';
|
|||
export function useTeamGraphAdapter(teamName: string): GraphDataPort {
|
||||
const adapterRef = useRef<TeamGraphAdapter>(TeamGraphAdapter.create());
|
||||
|
||||
const { teamData, spawnStatuses, leadContext } = useStore(
|
||||
const { teamData, spawnStatuses, leadContext, pendingApprovals } = useStore(
|
||||
useShallow((s) => ({
|
||||
teamData: s.selectedTeamData,
|
||||
spawnStatuses: teamName ? s.memberSpawnStatusesByTeam[teamName] : undefined,
|
||||
leadContext: teamName ? s.leadContextByTeam[teamName] : undefined,
|
||||
pendingApprovals: s.pendingApprovals,
|
||||
}))
|
||||
);
|
||||
|
||||
const pendingApprovalAgents = useMemo(() => {
|
||||
const agents = new Set<string>();
|
||||
for (const a of pendingApprovals) {
|
||||
if (a.source !== 'lead') agents.add(a.source);
|
||||
}
|
||||
return agents;
|
||||
}, [pendingApprovals]);
|
||||
|
||||
return useMemo(
|
||||
() => adapterRef.current.adapt(teamData, teamName, spawnStatuses, leadContext),
|
||||
[teamData, teamName, spawnStatuses, leadContext]
|
||||
() =>
|
||||
adapterRef.current.adapt(
|
||||
teamData,
|
||||
teamName,
|
||||
spawnStatuses,
|
||||
leadContext,
|
||||
pendingApprovalAgents
|
||||
),
|
||||
[teamData, teamName, spawnStatuses, leadContext, pendingApprovalAgents]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue