fix(team): re-add control_response via stdin for teammate permissions
Belt-and-suspenders approach:
1. Settings file: handles all FUTURE calls (teammate finds rule on retry)
2. control_response via stdin: may unblock CURRENT waiting prompt
(now includes updatedInput: {} which was the previous ZodError fix)
Without #2, approved teammates stay stuck until team restart because
the CLI doesn't hot-reload settings.local.json for pending prompts.
This commit is contained in:
parent
9a1ba76324
commit
bd242fac5a
10 changed files with 166 additions and 22 deletions
|
|
@ -52,7 +52,7 @@ export function drawAgents(
|
||||||
|
|
||||||
// Pending approval indicator: pulsing amber ring
|
// Pending approval indicator: pulsing amber ring
|
||||||
if (node.pendingApproval) {
|
if (node.pendingApproval) {
|
||||||
const pulseAlpha = 0.3 + 0.35 * Math.sin(time * 3);
|
const pulseAlpha = 0.3 + 0.35 * Math.sin(time * 7);
|
||||||
const ringR = r + 5;
|
const ringR = r + 5;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, ringR, 0, Math.PI * 2);
|
ctx.arc(x, y, ringR, 0, Math.PI * 2);
|
||||||
|
|
@ -367,20 +367,31 @@ function drawBreathing(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Avatar image cache ─────────────────────────────────────────────────────
|
// ─── Avatar image cache with LRU eviction ───────────────────────────────────
|
||||||
|
|
||||||
|
const AVATAR_CACHE_MAX = 100;
|
||||||
const avatarCache = new Map<string, HTMLImageElement>();
|
const avatarCache = new Map<string, HTMLImageElement>();
|
||||||
const avatarLoading = new Set<string>();
|
const avatarLoading = new Set<string>();
|
||||||
|
|
||||||
function getAvatarImage(url: string): HTMLImageElement | null {
|
function getAvatarImage(url: string): HTMLImageElement | null {
|
||||||
const cached = avatarCache.get(url);
|
const cached = avatarCache.get(url);
|
||||||
if (cached) return cached;
|
if (cached) {
|
||||||
|
// Move to end (most recently used)
|
||||||
|
avatarCache.delete(url);
|
||||||
|
avatarCache.set(url, cached);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
if (avatarLoading.has(url)) return null;
|
if (avatarLoading.has(url)) return null;
|
||||||
|
|
||||||
avatarLoading.add(url);
|
avatarLoading.add(url);
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.crossOrigin = 'anonymous';
|
img.crossOrigin = 'anonymous';
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
|
// Evict oldest entry if over limit
|
||||||
|
if (avatarCache.size >= AVATAR_CACHE_MAX) {
|
||||||
|
const first = avatarCache.keys().next().value;
|
||||||
|
if (first != null) avatarCache.delete(first);
|
||||||
|
}
|
||||||
avatarCache.set(url, img);
|
avatarCache.set(url, img);
|
||||||
avatarLoading.delete(url);
|
avatarLoading.delete(url);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ export interface VisualEffect {
|
||||||
color: string;
|
color: string;
|
||||||
age: number;
|
age: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
|
/** Node radius for scaling the effect */
|
||||||
|
nodeRadius?: number;
|
||||||
particles?: ShatterParticle[];
|
particles?: ShatterParticle[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,8 +31,8 @@ interface ShatterParticle {
|
||||||
/**
|
/**
|
||||||
* Create a spawn effect at position.
|
* Create a spawn effect at position.
|
||||||
*/
|
*/
|
||||||
export function createSpawnEffect(x: number, y: number, color: string): VisualEffect {
|
export function createSpawnEffect(x: number, y: number, color: string, nodeRadius?: number): VisualEffect {
|
||||||
return { type: 'spawn', x, y, color, age: 0, duration: 0.8 };
|
return { type: 'spawn', x, y, color, age: 0, duration: 0.8, nodeRadius };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -76,7 +78,8 @@ export function drawEffects(
|
||||||
|
|
||||||
function drawSpawnEffect(ctx: CanvasRenderingContext2D, fx: VisualEffect, progress: number): void {
|
function drawSpawnEffect(ctx: CanvasRenderingContext2D, fx: VisualEffect, progress: number): void {
|
||||||
const alpha = SPAWN_FX.maxAlpha * (1 - progress);
|
const alpha = SPAWN_FX.maxAlpha * (1 - progress);
|
||||||
const ringR = SPAWN_FX.ringStart + SPAWN_FX.ringExpand * progress;
|
const baseR = fx.nodeRadius ?? SPAWN_FX.ringStart;
|
||||||
|
const ringR = baseR + SPAWN_FX.ringExpand * progress;
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.globalAlpha = alpha;
|
ctx.globalAlpha = alpha;
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,11 @@ function drawTaskPill(
|
||||||
ctx.translate(x, y);
|
ctx.translate(x, y);
|
||||||
ctx.scale(scale, scale);
|
ctx.scale(scale, scale);
|
||||||
|
|
||||||
// Shadow — stronger for attention tasks
|
// Shadow — stronger for attention tasks, red for blocked
|
||||||
ctx.shadowColor = hexWithAlpha(statusColor, 0.25);
|
ctx.shadowColor = node.isBlocked
|
||||||
ctx.shadowBlur = needsAttention ? 12 : 4;
|
? hexWithAlpha(COLORS.edgeBlocking, 0.3)
|
||||||
|
: hexWithAlpha(statusColor, 0.25);
|
||||||
|
ctx.shadowBlur = needsAttention || node.isBlocked ? 12 : 4;
|
||||||
|
|
||||||
// Background fill
|
// Background fill
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
|
@ -98,13 +100,26 @@ function drawTaskPill(
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.shadowBlur = 0;
|
ctx.shadowBlur = 0;
|
||||||
|
|
||||||
// Border
|
// Border — red for blocked tasks
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.roundRect(-halfW, -halfH, w, h, r);
|
ctx.roundRect(-halfW, -halfH, w, h, r);
|
||||||
ctx.strokeStyle = hexWithAlpha(statusColor, isSelected ? 0.8 : 0.5);
|
if (node.isBlocked) {
|
||||||
ctx.lineWidth = isSelected ? 2 : 1;
|
ctx.strokeStyle = hexWithAlpha(COLORS.edgeBlocking, isSelected ? 0.9 : 0.7);
|
||||||
|
ctx.lineWidth = isSelected ? 2.5 : 1.8;
|
||||||
|
} else {
|
||||||
|
ctx.strokeStyle = hexWithAlpha(statusColor, isSelected ? 0.8 : 0.5);
|
||||||
|
ctx.lineWidth = isSelected ? 2 : 1;
|
||||||
|
}
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Blocked indicator — red left stripe
|
||||||
|
if (node.isBlocked) {
|
||||||
|
ctx.fillStyle = hexWithAlpha(COLORS.edgeBlocking, 0.6);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.roundRect(-halfW, -halfH, 4, h, [r, 0, 0, r]);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
// Review state overlay border — pulsing for review/needsFix, STATIC for approved
|
// Review state overlay border — pulsing for review/needsFix, STATIC for approved
|
||||||
if (reviewColor !== 'transparent') {
|
if (reviewColor !== 'transparent') {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
|
@ -203,6 +218,16 @@ export function drawColumnHeaders(
|
||||||
ctx.strokeStyle = hexWithAlpha(header.color, 0.2);
|
ctx.strokeStyle = hexWithAlpha(header.color, 0.2);
|
||||||
ctx.lineWidth = 0.5;
|
ctx.lineWidth = 0.5;
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
|
// Overflow badge: "+N more"
|
||||||
|
if (header.overflowCount > 0) {
|
||||||
|
const badgeText = `+${header.overflowCount} more`;
|
||||||
|
ctx.font = '7px monospace';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'top';
|
||||||
|
ctx.fillStyle = hexWithAlpha(header.color, 0.45);
|
||||||
|
ctx.fillText(badgeText, header.x, header.overflowY + 4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import {
|
||||||
type SimulationLinkDatum,
|
type SimulationLinkDatum,
|
||||||
} from 'd3-force';
|
} from 'd3-force';
|
||||||
import type { GraphNode, GraphEdge, GraphParticle, GraphNodeKind } from '../ports/types';
|
import type { GraphNode, GraphEdge, GraphParticle, GraphNodeKind } from '../ports/types';
|
||||||
import { FORCE, ANIM_SPEED } from '../constants/canvas-constants';
|
import { FORCE, ANIM_SPEED, NODE } from '../constants/canvas-constants';
|
||||||
import { getNodeStrategy } from '../strategies';
|
import { getNodeStrategy } from '../strategies';
|
||||||
import { createSpawnEffect, createCompleteEffect, type VisualEffect } from '../canvas/draw-effects';
|
import { createSpawnEffect, createCompleteEffect, type VisualEffect } from '../canvas/draw-effects';
|
||||||
import { getStateColor } from '../constants/colors';
|
import { getStateColor } from '../constants/colors';
|
||||||
|
|
@ -200,7 +200,8 @@ export function useGraphSimulation(): UseGraphSimulationResult {
|
||||||
// New node appeared → spawn effect (only if truly new, never seen before).
|
// New node appeared → spawn effect (only if truly new, never seen before).
|
||||||
// Nodes returning from filter (e.g. Tasks toggle OFF→ON) are already in allKnown.
|
// Nodes returning from filter (e.g. Tasks toggle OFF→ON) are already in allKnown.
|
||||||
if (!allKnown.has(node.id) && node.x != null && node.y != null) {
|
if (!allKnown.has(node.id) && node.x != null && node.y != null) {
|
||||||
state.effects.push(createSpawnEffect(node.x, node.y, node.color ?? getStateColor(node.state)));
|
const nodeR = node.kind === 'lead' ? NODE.radiusLead : node.kind === 'member' ? NODE.radiusMember : undefined;
|
||||||
|
state.effects.push(createSpawnEffect(node.x, node.y, node.color ?? getStateColor(node.state), nodeR));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Task completed → shatter effect
|
// Task completed → shatter effect
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ export interface KanbanColumnHeader {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
color: string;
|
color: string;
|
||||||
|
/** Number of hidden overflow tasks in this column */
|
||||||
|
overflowCount: number;
|
||||||
|
/** Y position for the overflow badge */
|
||||||
|
overflowY: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Zone info per owner for rendering headers */
|
/** Zone info per owner for rendering headers */
|
||||||
|
|
@ -125,6 +129,8 @@ export class KanbanLayoutEngine {
|
||||||
for (const [colIdx, col] of activeColumns.entries()) {
|
for (const [colIdx, col] of activeColumns.entries()) {
|
||||||
const colX = baseX + colIdx * columnWidth;
|
const colX = baseX + colIdx * columnWidth;
|
||||||
const config = COLUMN_LABELS[col.name] ?? { label: col.name, color: '#888' };
|
const config = COLUMN_LABELS[col.name] ?? { label: col.name, color: '#888' };
|
||||||
|
const overflow = Math.max(0, col.tasks.length - maxVisibleRows);
|
||||||
|
const visibleCount = Math.min(col.tasks.length, maxVisibleRows);
|
||||||
|
|
||||||
// Column header — centered over pill area (pill center = colX since drawTaskPill translates to x,y)
|
// Column header — centered over pill area (pill center = colX since drawTaskPill translates to x,y)
|
||||||
headers.push({
|
headers.push({
|
||||||
|
|
@ -132,6 +138,8 @@ export class KanbanLayoutEngine {
|
||||||
x: colX, // pill center = task.x = colX
|
x: colX, // pill center = task.x = colX
|
||||||
y: baseY,
|
y: baseY,
|
||||||
color: config.color,
|
color: config.color,
|
||||||
|
overflowCount: overflow,
|
||||||
|
overflowY: baseY + headerHeight + visibleCount * rowHeight,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Position tasks below header
|
// Position tasks below header
|
||||||
|
|
|
||||||
|
|
@ -90,10 +90,22 @@ export interface GraphNode {
|
||||||
reviewState?: 'none' | 'review' | 'needsFix' | 'approved';
|
reviewState?: 'none' | 'review' | 'needsFix' | 'approved';
|
||||||
/** Requires clarification indicator */
|
/** Requires clarification indicator */
|
||||||
needsClarification?: 'lead' | 'user' | null;
|
needsClarification?: 'lead' | 'user' | null;
|
||||||
|
/** Task is blocked by other tasks */
|
||||||
|
isBlocked?: boolean;
|
||||||
|
/** Display IDs of tasks blocking this one */
|
||||||
|
blockedByDisplayIds?: string[];
|
||||||
|
/** Display IDs of tasks this one blocks */
|
||||||
|
blocksDisplayIds?: string[];
|
||||||
|
|
||||||
// ─── Process-specific ──────────────────────────────────────────────────
|
// ─── Process-specific ──────────────────────────────────────────────────
|
||||||
/** Clickable URL for process */
|
/** Clickable URL for process */
|
||||||
processUrl?: string;
|
processUrl?: string;
|
||||||
|
/** Who registered the process */
|
||||||
|
processRegisteredBy?: string;
|
||||||
|
/** Command used to start the process */
|
||||||
|
processCommand?: string;
|
||||||
|
/** When the process was registered (ISO) */
|
||||||
|
processRegisteredAt?: string;
|
||||||
|
|
||||||
// ─── Force simulation (managed by the package internally) ──────────────
|
// ─── Force simulation (managed by the package internally) ──────────────
|
||||||
x?: number;
|
x?: number;
|
||||||
|
|
|
||||||
|
|
@ -201,9 +201,9 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(funct
|
||||||
}
|
}
|
||||||
drawEdges(ctx, visibleEdges, nodeMap, state.time, activeParticleEdges);
|
drawEdges(ctx, visibleEdges, nodeMap, state.time, activeParticleEdges);
|
||||||
|
|
||||||
// 2b. Particles (cap at 50 for performance)
|
// 2b. Particles (cap at 100 for performance)
|
||||||
const cappedParticles = state.particles.length > 50
|
const cappedParticles = state.particles.length > 100
|
||||||
? state.particles.slice(-50)
|
? state.particles.slice(-100)
|
||||||
: state.particles;
|
: state.particles;
|
||||||
drawParticles(ctx, cappedParticles, edgeMap, nodeMap, state.time);
|
drawParticles(ctx, cappedParticles, edgeMap, nodeMap, state.time);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6457,6 +6457,28 @@ export class TeamProvisioningService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also attempt control_response via stdin — the lead runtime MAY forward it
|
||||||
|
// to the teammate subprocess. This was broken before (missing updatedInput: {})
|
||||||
|
// but is now fixed. Belt-and-suspenders: settings handle future calls,
|
||||||
|
// control_response may unblock the CURRENT waiting prompt.
|
||||||
|
if (allow && run.child?.stdin?.writable) {
|
||||||
|
const controlResponse = {
|
||||||
|
type: 'control_response',
|
||||||
|
response: {
|
||||||
|
subtype: 'success',
|
||||||
|
request_id: requestId,
|
||||||
|
response: { behavior: 'allow', updatedInput: {} },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
run.child.stdin.write(JSON.stringify(controlResponse) + '\n', (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.warn(
|
||||||
|
`[${run.teamName}] control_response via stdin for teammate ${agentId} failed (non-critical): ${err.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -279,7 +279,7 @@ export class TeamGraphAdapter {
|
||||||
: undefined,
|
: undefined,
|
||||||
recentTools: (toolHistory?.[leadName] ?? [])
|
recentTools: (toolHistory?.[leadName] ?? [])
|
||||||
.filter((tool) => tool.state !== 'running' && !!tool.finishedAt)
|
.filter((tool) => tool.state !== 'running' && !!tool.finishedAt)
|
||||||
.slice(0, 3)
|
.slice(0, 5)
|
||||||
.map((tool) => ({
|
.map((tool) => ({
|
||||||
name: tool.toolName,
|
name: tool.toolName,
|
||||||
preview: tool.preview,
|
preview: tool.preview,
|
||||||
|
|
@ -346,7 +346,7 @@ export class TeamGraphAdapter {
|
||||||
: undefined,
|
: undefined,
|
||||||
recentTools: (toolHistory?.[member.name] ?? [])
|
recentTools: (toolHistory?.[member.name] ?? [])
|
||||||
.filter((tool) => tool.state !== 'running' && !!tool.finishedAt)
|
.filter((tool) => tool.state !== 'running' && !!tool.finishedAt)
|
||||||
.slice(0, 3)
|
.slice(0, 5)
|
||||||
.map((tool) => ({
|
.map((tool) => ({
|
||||||
name: tool.toolName,
|
name: tool.toolName,
|
||||||
preview: tool.preview,
|
preview: tool.preview,
|
||||||
|
|
@ -369,11 +369,32 @@ export class TeamGraphAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
#buildTaskNodes(nodes: GraphNode[], edges: GraphEdge[], data: TeamData, teamName: string): void {
|
#buildTaskNodes(nodes: GraphNode[], edges: GraphEdge[], data: TeamData, teamName: string): void {
|
||||||
|
// Build lookup tables for fast resolution
|
||||||
|
const completedTaskIds = new Set<string>();
|
||||||
|
const taskDisplayIds = new Map<string, string>();
|
||||||
|
for (const t of data.tasks) {
|
||||||
|
if (t.status === 'completed' || t.status === 'deleted') completedTaskIds.add(t.id);
|
||||||
|
taskDisplayIds.set(t.id, t.displayId ?? `#${t.id.slice(0, 6)}`);
|
||||||
|
}
|
||||||
|
|
||||||
for (const task of data.tasks) {
|
for (const task of data.tasks) {
|
||||||
if (task.status === 'deleted') continue;
|
if (task.status === 'deleted') continue;
|
||||||
const taskId = `task:${teamName}:${task.id}`;
|
const taskId = `task:${teamName}:${task.id}`;
|
||||||
const ownerMemberId = task.owner ? `member:${teamName}:${task.owner}` : null;
|
const ownerMemberId = task.owner ? `member:${teamName}:${task.owner}` : null;
|
||||||
|
|
||||||
|
// Task is blocked if any blockedBy task is still not completed
|
||||||
|
const isBlocked =
|
||||||
|
(task.blockedBy?.length ?? 0) > 0 &&
|
||||||
|
task.blockedBy!.some((id) => !completedTaskIds.has(id));
|
||||||
|
|
||||||
|
// Resolve display IDs for dependencies
|
||||||
|
const blockedByDisplayIds = task.blockedBy?.length
|
||||||
|
? task.blockedBy.map((id) => taskDisplayIds.get(id) ?? `#${id.slice(0, 6)}`)
|
||||||
|
: undefined;
|
||||||
|
const blocksDisplayIds = task.blocks?.length
|
||||||
|
? task.blocks.map((id) => taskDisplayIds.get(id) ?? `#${id.slice(0, 6)}`)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
nodes.push({
|
nodes.push({
|
||||||
id: taskId,
|
id: taskId,
|
||||||
kind: 'task',
|
kind: 'task',
|
||||||
|
|
@ -385,6 +406,9 @@ export class TeamGraphAdapter {
|
||||||
displayId: task.displayId ?? undefined,
|
displayId: task.displayId ?? undefined,
|
||||||
ownerId: ownerMemberId,
|
ownerId: ownerMemberId,
|
||||||
needsClarification: task.needsClarification ?? null,
|
needsClarification: task.needsClarification ?? null,
|
||||||
|
isBlocked,
|
||||||
|
blockedByDisplayIds,
|
||||||
|
blocksDisplayIds,
|
||||||
domainRef: { kind: 'task', teamName, taskId: task.id },
|
domainRef: { kind: 'task', teamName, taskId: task.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -453,6 +477,9 @@ export class TeamGraphAdapter {
|
||||||
label: proc.label,
|
label: proc.label,
|
||||||
state: 'active',
|
state: 'active',
|
||||||
processUrl: proc.url ?? undefined,
|
processUrl: proc.url ?? undefined,
|
||||||
|
processRegisteredBy: proc.registeredBy ?? undefined,
|
||||||
|
processCommand: proc.command ?? undefined,
|
||||||
|
processRegisteredAt: proc.registeredAt,
|
||||||
domainRef: { kind: 'process', teamName, processId: proc.id },
|
domainRef: { kind: 'process', teamName, processId: proc.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 { ExternalLink, Loader2, MessageSquare, Plus, User } from 'lucide-react';
|
import { Ban, ExternalLink, Loader2, MessageSquare, Plus, User } from 'lucide-react';
|
||||||
|
|
||||||
import type { GraphNode } from '@claude-teams/agent-graph';
|
import type { GraphNode } from '@claude-teams/agent-graph';
|
||||||
|
|
||||||
|
|
@ -78,8 +78,23 @@ export const GraphNodePopover = ({
|
||||||
|
|
||||||
// Process
|
// Process
|
||||||
return (
|
return (
|
||||||
<div className="min-w-[180px] rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-raised)] p-3 shadow-xl">
|
<div className="min-w-[180px] max-w-[260px] rounded-lg border border-[var(--color-border)] bg-[var(--color-surface-raised)] p-3 shadow-xl">
|
||||||
<div className="font-mono text-xs font-bold text-[var(--color-text)]">{node.label}</div>
|
<div className="font-mono text-xs font-bold text-[var(--color-text)]">{node.label}</div>
|
||||||
|
{node.processCommand && (
|
||||||
|
<div className="mt-1 truncate font-mono text-[10px] text-[var(--color-text-muted)]">
|
||||||
|
$ {node.processCommand}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="mt-2 space-y-0.5 text-[10px] text-[var(--color-text-muted)]">
|
||||||
|
{node.processRegisteredBy && (
|
||||||
|
<div>
|
||||||
|
Started by: <span className="text-[var(--color-text)]">{node.processRegisteredBy}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{node.processRegisteredAt && (
|
||||||
|
<div>At: {new Date(node.processRegisteredAt).toLocaleTimeString()}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{node.processUrl && (
|
{node.processUrl && (
|
||||||
<a
|
<a
|
||||||
href={node.processUrl}
|
href={node.processUrl}
|
||||||
|
|
@ -254,7 +269,7 @@ const MemberPopoverContent = ({
|
||||||
Recent tools
|
Recent tools
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{node.recentTools.slice(0, 3).map((tool) => {
|
{node.recentTools.slice(0, 5).map((tool) => {
|
||||||
const shortName = formatToolName(tool.name);
|
const shortName = formatToolName(tool.name);
|
||||||
const shortPreview = formatToolPreview(tool.preview);
|
const shortPreview = formatToolPreview(tool.preview);
|
||||||
return (
|
return (
|
||||||
|
|
@ -368,6 +383,14 @@ const TaskPopoverContent = ({
|
||||||
{node.reviewState}
|
{node.reviewState}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
{node.isBlocked && (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-red-500/30 px-1.5 py-0 text-[10px] text-red-400"
|
||||||
|
>
|
||||||
|
<Ban size={10} className="mr-0.5" /> blocked
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
{node.needsClarification && (
|
{node.needsClarification && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -378,6 +401,18 @@ const TaskPopoverContent = ({
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Task dependencies */}
|
||||||
|
{node.blockedByDisplayIds && node.blockedByDisplayIds.length > 0 && (
|
||||||
|
<div className="mt-2 text-[10px] text-[var(--color-text-muted)]">
|
||||||
|
<span className="text-red-400">Blocked by:</span> {node.blockedByDisplayIds.join(', ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{node.blocksDisplayIds && node.blocksDisplayIds.length > 0 && (
|
||||||
|
<div className="mt-1 text-[10px] text-[var(--color-text-muted)]">
|
||||||
|
<span className="text-amber-400">Blocks:</span> {node.blocksDisplayIds.join(', ')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue