@agenticmail/enterprise 0.5.200 → 0.5.202

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,745 @@
1
+ import { h, useState, useEffect, useCallback, useRef, Fragment, useApp, engineCall, getOrgId } from '../components/utils.js';
2
+ import { I } from '../components/icons.js';
3
+ import { HelpButton } from '../components/help-button.js';
4
+
5
+ // ─── Layout Constants (matches org-chart) ────────────────
6
+ const NODE_W = 240;
7
+ const NODE_H = 80;
8
+ const H_GAP = 32;
9
+ const V_GAP = 64;
10
+ const PAD = 60;
11
+
12
+ // ─── Colors ──────────────────────────────────────────────
13
+ const STATUS_COLORS = {
14
+ created: '#6366f1',
15
+ assigned: '#f59e0b',
16
+ in_progress: '#06b6d4',
17
+ completed: '#22c55e',
18
+ failed: '#ef4444',
19
+ cancelled: '#6b7394',
20
+ };
21
+ const PRIORITY_COLORS = { urgent: '#ef4444', high: '#f59e0b', normal: '#6366f1', low: '#6b7394' };
22
+ const ACCENT = '#6366f1';
23
+ const EDGE_COLOR = 'rgba(255,255,255,0.25)';
24
+ const EDGE_HIGHLIGHT = 'rgba(99,102,241,0.7)';
25
+ const BG = '#0a0c14';
26
+
27
+ // ─── Tree Layout ─────────────────────────────────────────
28
+ // Groups tasks by agent → builds hierarchy: agent nodes at top, task nodes as children
29
+ // Sub-tasks appear as children of their parent task
30
+ function layoutPipeline(tasks, agents) {
31
+ if (!tasks || !tasks.length) return { positioned: [], edges: [], width: 0, height: 0 };
32
+
33
+ // Group tasks by agent
34
+ var agentMap = new Map();
35
+ tasks.forEach(function(t) {
36
+ var key = t.assignedTo || 'unassigned';
37
+ if (!agentMap.has(key)) agentMap.set(key, { agentId: key, name: t.assignedToName || key, tasks: [], children: [], x: 0, y: 0, subtreeW: 0, isAgent: true });
38
+ agentMap.get(key).tasks.push(t);
39
+ });
40
+
41
+ // Build task nodes under each agent
42
+ var allNodes = [];
43
+ var edgeList = [];
44
+ var agentRoots = [];
45
+
46
+ agentMap.forEach(function(agent) {
47
+ agent.subtreeW = 0;
48
+ var taskNodes = [];
49
+ var taskById = new Map();
50
+
51
+ // Create task nodes
52
+ agent.tasks.forEach(function(t) {
53
+ var node = { id: t.id, task: t, children: [], x: 0, y: 0, subtreeW: 0, isAgent: false, agentId: agent.agentId };
54
+ taskById.set(t.id, node);
55
+ taskNodes.push(node);
56
+ });
57
+
58
+ // Link sub-tasks to parents
59
+ var rootTasks = [];
60
+ taskNodes.forEach(function(tn) {
61
+ if (tn.task.parentTaskId && taskById.has(tn.task.parentTaskId)) {
62
+ taskById.get(tn.task.parentTaskId).children.push(tn);
63
+ } else {
64
+ rootTasks.push(tn);
65
+ }
66
+ });
67
+
68
+ agent.children = rootTasks;
69
+ agentRoots.push(agent);
70
+ });
71
+
72
+ // Compute subtree widths
73
+ function computeW(node) {
74
+ var w = node.isAgent ? NODE_W : NODE_W;
75
+ if (node.children.length === 0) { node.subtreeW = w; return w; }
76
+ var total = 0;
77
+ node.children.forEach(function(c) { total += computeW(c); });
78
+ total += (node.children.length - 1) * H_GAP;
79
+ node.subtreeW = Math.max(w, total);
80
+ return node.subtreeW;
81
+ }
82
+
83
+ // Assign positions
84
+ function assignPos(node, x, y) {
85
+ var w = node.isAgent ? NODE_W : NODE_W;
86
+ node.x = x + node.subtreeW / 2 - w / 2;
87
+ node.y = y;
88
+ if (node.children.length === 0) return;
89
+ var childrenW = node.children.reduce(function(s, c) { return s + c.subtreeW; }, 0) + (node.children.length - 1) * H_GAP;
90
+ var cx = node.x + w / 2 - childrenW / 2;
91
+ node.children.forEach(function(c) {
92
+ assignPos(c, cx, y + NODE_H + V_GAP);
93
+ cx += c.subtreeW + H_GAP;
94
+ });
95
+ }
96
+
97
+ // Layout agent roots side by side
98
+ agentRoots.forEach(function(r) { computeW(r); });
99
+ var cx = PAD;
100
+ agentRoots.forEach(function(r) {
101
+ assignPos(r, cx, PAD);
102
+ cx += r.subtreeW + H_GAP * 2;
103
+ });
104
+
105
+ // Flatten + collect edges
106
+ var maxX = 0, maxY = 0;
107
+ function flatten(node, parent) {
108
+ allNodes.push(node);
109
+ var w = NODE_W;
110
+ maxX = Math.max(maxX, node.x + w);
111
+ maxY = Math.max(maxY, node.y + NODE_H);
112
+ if (parent) edgeList.push({ parent: parent, child: node });
113
+ node.children.forEach(function(c) { flatten(c, node); });
114
+ }
115
+ agentRoots.forEach(function(r) { flatten(r, null); });
116
+
117
+ return { positioned: allNodes, edges: edgeList, width: maxX + PAD, height: maxY + PAD + 40 };
118
+ }
119
+
120
+ // ─── SVG Edge (curved, matches org-chart) ────────────────
121
+ function edgePath(parent, child) {
122
+ var x1 = parent.x + NODE_W / 2;
123
+ var y1 = parent.y + NODE_H;
124
+ var x2 = child.x + NODE_W / 2;
125
+ var y2 = child.y;
126
+ var midY = y1 + (y2 - y1) * 0.5;
127
+ return 'M ' + x1 + ' ' + y1 + ' C ' + x1 + ' ' + midY + ', ' + x2 + ' ' + midY + ', ' + x2 + ' ' + y2;
128
+ }
129
+
130
+ // ─── Helpers ─────────────────────────────────────────────
131
+ function timeAgo(ts) {
132
+ if (!ts) return '-';
133
+ var diff = Date.now() - new Date(ts).getTime();
134
+ if (diff < 5000) return 'just now';
135
+ if (diff < 60000) return Math.floor(diff / 1000) + 's ago';
136
+ if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
137
+ if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
138
+ return Math.floor(diff / 86400000) + 'd ago';
139
+ }
140
+
141
+ function formatDuration(ms) {
142
+ if (!ms) return '-';
143
+ var s = Math.floor(ms / 1000);
144
+ if (s < 60) return s + 's';
145
+ var m = Math.floor(s / 60);
146
+ if (m < 60) return m + 'm ' + (s % 60) + 's';
147
+ return Math.floor(m / 60) + 'h ' + (m % 60) + 'm';
148
+ }
149
+
150
+ var CATEGORY_ICONS = { email: '\u2709', research: '\uD83D\uDD0D', meeting: '\uD83D\uDCC5', workflow: '\u2699', writing: '\u270F', deployment: '\uD83D\uDE80', review: '\u2714', monitoring: '\uD83D\uDCE1', custom: '\uD83D\uDCCB' };
151
+
152
+ function tagStyle(color) {
153
+ return { fontSize: 9, fontWeight: 600, padding: '1px 5px', borderRadius: 4, background: color + '22', color: color, letterSpacing: '0.02em' };
154
+ }
155
+
156
+ var toolbarBtnStyle = {
157
+ background: 'rgba(255,255,255,0.08)',
158
+ border: '1px solid rgba(255,255,255,0.12)',
159
+ borderRadius: 6,
160
+ color: '#fff',
161
+ fontSize: 14,
162
+ fontWeight: 600,
163
+ padding: '4px 8px',
164
+ cursor: 'pointer',
165
+ lineHeight: '1.2',
166
+ };
167
+
168
+ function legendDot(color, label) {
169
+ return h('div', { style: { display: 'flex', alignItems: 'center', gap: 4 } },
170
+ h('div', { style: { width: 8, height: 8, borderRadius: '50%', background: color } }),
171
+ h('span', { style: { color: 'rgba(255,255,255,0.5)', fontSize: 12 } }, label)
172
+ );
173
+ }
174
+
175
+ function tooltipRow(label, value, color) {
176
+ return h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: 11 } },
177
+ h('span', { style: { color: 'rgba(255,255,255,0.4)' } }, label),
178
+ h('span', { style: { fontWeight: 600, color: color || '#fff' } }, value)
179
+ );
180
+ }
181
+
182
+ // ─── Help tooltip styles ─────────────────────────────────
183
+ var _h4 = { marginTop: 16, marginBottom: 8, fontSize: 14 };
184
+ var _ul = { paddingLeft: 20, margin: '4px 0 8px' };
185
+ var _tip = { marginTop: 12, padding: 12, background: 'var(--bg-secondary, #1e293b)', borderRadius: 'var(--radius, 8px)', fontSize: 13 };
186
+
187
+ // ─── Task Detail Modal ───────────────────────────────────
188
+ function TaskDetail(props) {
189
+ var task = props.task;
190
+ var onClose = props.onClose;
191
+ var onCancel = props.onCancel;
192
+ if (!task) return null;
193
+ var statusColor = STATUS_COLORS[task.status] || '#6b7394';
194
+
195
+ return h('div', { className: 'modal-overlay', onClick: onClose },
196
+ h('div', { className: 'modal', onClick: function(e) { e.stopPropagation(); }, style: { width: 560, maxHeight: '80vh', overflow: 'auto' } },
197
+ h('div', { className: 'modal-header' },
198
+ h('h2', null, task.title),
199
+ h('button', { className: 'btn btn-ghost btn-icon', onClick: onClose }, '\u00D7')
200
+ ),
201
+ h('div', { className: 'modal-body', style: { padding: 20 } },
202
+ h('div', { style: { display: 'flex', gap: 8, marginBottom: 16, alignItems: 'center', flexWrap: 'wrap' } },
203
+ h('span', { style: { padding: '3px 10px', borderRadius: 12, fontSize: 12, fontWeight: 600, background: statusColor + '22', color: statusColor, border: '1px solid ' + statusColor + '44' } }, task.status.replace('_', ' ').toUpperCase()),
204
+ h('span', { style: { padding: '3px 10px', borderRadius: 12, fontSize: 12, background: (PRIORITY_COLORS[task.priority] || '#6366f1') + '22', color: PRIORITY_COLORS[task.priority] || '#6366f1' } }, task.priority.toUpperCase()),
205
+ h('span', { style: { padding: '3px 10px', borderRadius: 12, fontSize: 12, background: 'var(--bg-tertiary)', color: 'var(--text-muted)' } }, (CATEGORY_ICONS[task.category] || '') + ' ' + task.category)
206
+ ),
207
+ task.description && h('div', { style: { marginBottom: 16, fontSize: 13, lineHeight: 1.6, color: 'var(--text-secondary)' } }, task.description),
208
+ task.status === 'in_progress' && h('div', { style: { marginBottom: 16 } },
209
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 4 } }, h('span', null, 'Progress'), h('span', null, task.progress + '%')),
210
+ h('div', { style: { height: 6, background: 'var(--border)', borderRadius: 3, overflow: 'hidden' } },
211
+ h('div', { style: { height: '100%', width: task.progress + '%', background: STATUS_COLORS.in_progress, borderRadius: 3 } })
212
+ )
213
+ ),
214
+ h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px 24px', fontSize: 13, marginBottom: 16 } },
215
+ h('div', null, h('div', { style: { color: 'var(--text-muted)', fontSize: 11, marginBottom: 2 } }, 'Assigned To'), h('div', null, task.assignedToName || task.assignedTo || '-')),
216
+ h('div', null, h('div', { style: { color: 'var(--text-muted)', fontSize: 11, marginBottom: 2 } }, 'Created By'), h('div', null, task.createdByName || task.createdBy || '-')),
217
+ h('div', null, h('div', { style: { color: 'var(--text-muted)', fontSize: 11, marginBottom: 2 } }, 'Created'), h('div', null, task.createdAt ? new Date(task.createdAt).toLocaleString() : '-')),
218
+ h('div', null, h('div', { style: { color: 'var(--text-muted)', fontSize: 11, marginBottom: 2 } }, 'Started'), h('div', null, task.startedAt ? new Date(task.startedAt).toLocaleString() : '-')),
219
+ h('div', null, h('div', { style: { color: 'var(--text-muted)', fontSize: 11, marginBottom: 2 } }, 'Completed'), h('div', null, task.completedAt ? new Date(task.completedAt).toLocaleString() : '-')),
220
+ h('div', null, h('div', { style: { color: 'var(--text-muted)', fontSize: 11, marginBottom: 2 } }, 'Duration'), h('div', null, formatDuration(task.actualDurationMs))),
221
+ h('div', null, h('div', { style: { color: 'var(--text-muted)', fontSize: 11, marginBottom: 2 } }, 'Model'), h('div', null, task.modelUsed || task.model || '-')),
222
+ h('div', null, h('div', { style: { color: 'var(--text-muted)', fontSize: 11, marginBottom: 2 } }, 'Tokens / Cost'), h('div', null, (task.tokensUsed || 0).toLocaleString() + ' / $' + (task.costUsd || 0).toFixed(4)))
223
+ ),
224
+ task.error && h('div', { style: { padding: 12, background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.3)', borderRadius: 'var(--radius)', marginBottom: 16, fontSize: 13, color: '#ef4444' } }, h('strong', null, 'Error: '), task.error),
225
+ task.result && h('div', { style: { marginBottom: 16 } },
226
+ h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 4 } }, 'Result'),
227
+ h('pre', { style: { fontSize: 12, background: 'var(--bg-tertiary)', padding: 12, borderRadius: 'var(--radius)', overflow: 'auto', maxHeight: 200 } }, JSON.stringify(task.result, null, 2))
228
+ ),
229
+ (task.status === 'created' || task.status === 'assigned' || task.status === 'in_progress') && h('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 16, borderTop: '1px solid var(--border)', paddingTop: 16 } },
230
+ h('button', { className: 'btn btn-danger btn-sm', onClick: function() { onCancel(task.id); } }, 'Cancel Task')
231
+ )
232
+ )
233
+ )
234
+ );
235
+ }
236
+
237
+ // ─── Main Page ───────────────────────────────────────────
238
+ export function TaskPipelinePage() {
239
+ var app = useApp();
240
+ var toast = app.toast;
241
+ var _tasks = useState([]);
242
+ var tasks = _tasks[0]; var setTasks = _tasks[1];
243
+ var _stats = useState({ created: 0, assigned: 0, inProgress: 0, completed: 0, failed: 0, cancelled: 0, total: 0 });
244
+ var stats = _stats[0]; var setStats = _stats[1];
245
+ var _loading = useState(true);
246
+ var loading = _loading[0]; var setLoading = _loading[1];
247
+ var _selectedTask = useState(null);
248
+ var selectedTask = _selectedTask[0]; var setSelectedTask = _selectedTask[1];
249
+ var _hoveredId = useState(null);
250
+ var hoveredId = _hoveredId[0]; var setHoveredId = _hoveredId[1];
251
+ var _mousePos = useState({ x: 0, y: 0 });
252
+ var mousePos = _mousePos[0]; var setMousePos = _mousePos[1];
253
+ var _zoom = useState(1);
254
+ var zoom = _zoom[0]; var setZoom = _zoom[1];
255
+ var _pan = useState({ x: 0, y: 0 });
256
+ var pan = _pan[0]; var setPan = _pan[1];
257
+ var _dragging = useState(false);
258
+ var dragging = _dragging[0]; var setDragging = _dragging[1];
259
+ var _dragStart = useState({ x: 0, y: 0 });
260
+ var dragStart = _dragStart[0]; var setDragStart = _dragStart[1];
261
+ var _filter = useState('active');
262
+ var filter = _filter[0]; var setFilter = _filter[1];
263
+ var containerRef = useRef(null);
264
+
265
+ var loadData = useCallback(function() {
266
+ setLoading(true);
267
+ Promise.all([
268
+ engineCall('/task-pipeline?limit=200'),
269
+ engineCall('/task-pipeline/stats'),
270
+ ]).then(function(res) {
271
+ setTasks(res[0]?.tasks || []);
272
+ setStats(res[1] || stats);
273
+ }).catch(function(err) {
274
+ console.error('[TaskPipeline] load:', err);
275
+ }).finally(function() { setLoading(false); });
276
+ }, []);
277
+
278
+ // SSE
279
+ useEffect(function() {
280
+ loadData();
281
+ var baseUrl = window.__ENGINE_BASE || '/api/engine';
282
+ var es;
283
+ try {
284
+ es = new EventSource(baseUrl + '/task-pipeline/stream');
285
+ es.onmessage = function(e) {
286
+ try {
287
+ var event = JSON.parse(e.data);
288
+ if (event.type === 'init') {
289
+ if (event.tasks) setTasks(function(prev) {
290
+ var map = new Map();
291
+ prev.forEach(function(t) { map.set(t.id, t); });
292
+ event.tasks.forEach(function(t) { map.set(t.id, t); });
293
+ return Array.from(map.values()).sort(function(a, b) { return new Date(b.createdAt) - new Date(a.createdAt); });
294
+ });
295
+ if (event.stats) setStats(event.stats);
296
+ } else if (event.task) {
297
+ setTasks(function(prev) {
298
+ var idx = prev.findIndex(function(t) { return t.id === event.task.id; });
299
+ if (idx >= 0) { var next = prev.slice(); next[idx] = event.task; return next; }
300
+ return [event.task].concat(prev);
301
+ });
302
+ setSelectedTask(function(prev) {
303
+ if (prev && prev.id === event.task.id) return event.task;
304
+ return prev;
305
+ });
306
+ }
307
+ } catch (err) {}
308
+ };
309
+ } catch (err) {}
310
+ return function() { if (es) es.close(); };
311
+ }, []);
312
+
313
+ // Periodic stats
314
+ useEffect(function() {
315
+ var iv = setInterval(function() {
316
+ engineCall('/task-pipeline/stats').then(function(s) { if (s) setStats(s); }).catch(function() {});
317
+ }, 30000);
318
+ return function() { clearInterval(iv); };
319
+ }, []);
320
+
321
+ var cancelTask = useCallback(function(taskId) {
322
+ engineCall('/task-pipeline/' + taskId + '/cancel', { method: 'POST' }).then(function() {
323
+ toast('Task cancelled', 'success');
324
+ setSelectedTask(null);
325
+ loadData();
326
+ }).catch(function(err) { toast(err.message || 'Failed', 'error'); });
327
+ }, []);
328
+
329
+ // Filter
330
+ var filtered = tasks.filter(function(t) {
331
+ if (filter === 'active') return t.status === 'created' || t.status === 'assigned' || t.status === 'in_progress';
332
+ if (filter === 'completed') return t.status === 'completed';
333
+ if (filter === 'failed') return t.status === 'failed' || t.status === 'cancelled';
334
+ return true;
335
+ });
336
+
337
+ // Layout
338
+ var layout = layoutPipeline(filtered);
339
+ var positioned = layout.positioned;
340
+ var edges = layout.edges;
341
+ var treeW = layout.width;
342
+ var treeH = layout.height;
343
+
344
+ // Zoom
345
+ var handleWheel = useCallback(function(e) {
346
+ e.preventDefault();
347
+ var delta = e.deltaY > 0 ? -0.08 : 0.08;
348
+ setZoom(function(z) { return Math.min(3, Math.max(0.15, z + delta)); });
349
+ }, []);
350
+
351
+ // Pan
352
+ var handleMouseDown = useCallback(function(e) {
353
+ if (e.button !== 0) return;
354
+ if (e.target.closest('.task-node')) return;
355
+ setDragging(true);
356
+ setDragStart({ x: e.clientX - pan.x, y: e.clientY - pan.y });
357
+ }, [pan]);
358
+
359
+ var handleMouseMove = useCallback(function(e) {
360
+ if (!dragging) return;
361
+ setPan({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y });
362
+ }, [dragging, dragStart]);
363
+
364
+ var handleMouseUp = useCallback(function() { setDragging(false); }, []);
365
+
366
+ useEffect(function() {
367
+ if (dragging) {
368
+ window.addEventListener('mousemove', handleMouseMove);
369
+ window.addEventListener('mouseup', handleMouseUp);
370
+ return function() { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); };
371
+ }
372
+ }, [dragging, handleMouseMove, handleMouseUp]);
373
+
374
+ // Fit
375
+ var fitToView = useCallback(function() {
376
+ if (!containerRef.current || !treeW || !treeH) return;
377
+ var rect = containerRef.current.getBoundingClientRect();
378
+ var scaleX = (rect.width - 40) / treeW;
379
+ var scaleY = (rect.height - 40) / treeH;
380
+ var scale = Math.min(scaleX, scaleY, 1.5);
381
+ setZoom(scale);
382
+ setPan({ x: (rect.width - treeW * scale) / 2, y: (rect.height - treeH * scale) / 2 });
383
+ }, [treeW, treeH]);
384
+
385
+ useEffect(function() { if (positioned.length > 0) fitToView(); }, [positioned.length]);
386
+
387
+ // Connected highlight
388
+ var getConnected = useCallback(function(id) {
389
+ var connected = new Set([id]);
390
+ var byId = new Map();
391
+ positioned.forEach(function(n) { byId.set(n.id || n.agentId, n); });
392
+ // Children
393
+ function addDesc(node) {
394
+ (node.children || []).forEach(function(c) { connected.add(c.id || c.agentId); addDesc(c); });
395
+ }
396
+ var node = byId.get(id);
397
+ if (node) addDesc(node);
398
+ // Parent edges
399
+ edges.forEach(function(e) {
400
+ var cId = e.child.id || e.child.agentId;
401
+ var pId = e.parent.id || e.parent.agentId;
402
+ if (cId === id) connected.add(pId);
403
+ });
404
+ return connected;
405
+ }, [positioned, edges]);
406
+
407
+ var connected = hoveredId ? getConnected(hoveredId) : null;
408
+
409
+ // Hovered node for tooltip
410
+ var hoveredNode = hoveredId ? positioned.find(function(n) { return (n.id || n.agentId) === hoveredId; }) : null;
411
+
412
+ if (loading) return h('div', { style: { padding: 40, textAlign: 'center', color: 'var(--text-muted)' } }, 'Loading task pipeline...');
413
+
414
+ if (positioned.length === 0) return h('div', { style: { height: '100%', display: 'flex', flexDirection: 'column', background: BG, borderRadius: 'var(--radius-lg)', overflow: 'hidden' } },
415
+ // Toolbar even when empty
416
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 12, padding: '12px 20px', borderBottom: '1px solid rgba(255,255,255,0.08)', background: 'rgba(0,0,0,0.3)' } },
417
+ h('div', { style: { fontWeight: 700, fontSize: 16, color: '#fff', display: 'flex', alignItems: 'center', gap: 8 } }, I.workflow(), ' Task Pipeline'),
418
+ ),
419
+ h('div', { style: { flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' } },
420
+ h('div', { style: { fontSize: 48, marginBottom: 16 } }, '\uD83D\uDCCB'),
421
+ h('div', { style: { fontSize: 18, fontWeight: 600, marginBottom: 8, color: '#fff' } }, 'No Tasks in Pipeline'),
422
+ h('div', { style: { color: 'rgba(255,255,255,0.5)' } }, 'Tasks will appear here as agents are assigned work.')
423
+ )
424
+ );
425
+
426
+ return h('div', { style: { height: '100%', display: 'flex', flexDirection: 'column', background: BG, borderRadius: 'var(--radius-lg)', overflow: 'hidden' } },
427
+ // Toolbar
428
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 12, padding: '12px 20px', borderBottom: '1px solid rgba(255,255,255,0.08)', background: 'rgba(0,0,0,0.3)', flexShrink: 0, flexWrap: 'wrap' } },
429
+ h('div', { style: { fontWeight: 700, fontSize: 16, color: '#fff', display: 'flex', alignItems: 'center', gap: 8 } },
430
+ I.workflow(), ' Task Pipeline',
431
+ h(HelpButton, { label: 'Task Pipeline' },
432
+ h('p', null, 'Visual hierarchy of all agent tasks in your organization. Tasks flow from agents (top) to individual tasks and sub-tasks (below), connected by arrows.'),
433
+ h('h4', { style: _h4 }, 'Interactions'),
434
+ h('ul', { style: _ul },
435
+ h('li', null, h('strong', null, 'Hover'), ' — Highlights the task chain and shows detail tooltip'),
436
+ h('li', null, h('strong', null, 'Click'), ' — Opens full task detail modal'),
437
+ h('li', null, h('strong', null, 'Scroll'), ' — Zoom in/out'),
438
+ h('li', null, h('strong', null, 'Drag'), ' — Pan the canvas')
439
+ ),
440
+ h('h4', { style: _h4 }, 'Task Lifecycle'),
441
+ h('ul', { style: _ul },
442
+ h('li', null, h('strong', null, 'Queued'), ' — Waiting to be picked up'),
443
+ h('li', null, h('strong', null, 'Assigned'), ' — Agent selected, about to start'),
444
+ h('li', null, h('strong', null, 'In Progress'), ' — Actively executing'),
445
+ h('li', null, h('strong', null, 'Completed'), ' — Finished successfully'),
446
+ h('li', null, h('strong', null, 'Failed'), ' — Encountered an error')
447
+ ),
448
+ h('div', { style: _tip }, 'Tip: Sub-tasks appear as children of their parent task. The arrow system shows task delegation flow.')
449
+ )
450
+ ),
451
+ // Live dot
452
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 4 } },
453
+ h('div', { style: { width: 8, height: 8, borderRadius: '50%', background: '#22c55e', animation: 'pulse 2s infinite' } }),
454
+ h('span', { style: { color: 'rgba(255,255,255,0.4)', fontSize: 12 } }, 'Live')
455
+ ),
456
+ // Stats
457
+ h('div', { style: { color: 'rgba(255,255,255,0.4)', fontSize: 12 } },
458
+ (stats.inProgress || 0) + ' active \u00B7 ' + (stats.completed || 0) + ' done \u00B7 ' + (stats.total || 0) + ' total'
459
+ ),
460
+ h('div', { style: { flex: 1 } }),
461
+ // Filter
462
+ ['active', 'all', 'completed', 'failed'].map(function(f) {
463
+ return h('button', {
464
+ key: f,
465
+ onClick: function() { setFilter(f); },
466
+ style: Object.assign({}, toolbarBtnStyle, { fontSize: 11, padding: '4px 10px', background: filter === f ? 'rgba(99,102,241,0.3)' : toolbarBtnStyle.background, borderColor: filter === f ? 'rgba(99,102,241,0.5)' : toolbarBtnStyle.border })
467
+ }, f.charAt(0).toUpperCase() + f.slice(1));
468
+ }),
469
+ h('div', { style: { width: 1, height: 16, background: 'rgba(255,255,255,0.12)' } }),
470
+ // Legend
471
+ legendDot(STATUS_COLORS.in_progress, 'Active'),
472
+ legendDot(STATUS_COLORS.assigned, 'Assigned'),
473
+ legendDot(STATUS_COLORS.completed, 'Done'),
474
+ legendDot(STATUS_COLORS.failed, 'Failed'),
475
+ h('div', { style: { width: 1, height: 16, background: 'rgba(255,255,255,0.12)' } }),
476
+ // Zoom
477
+ h('button', { onClick: function() { setZoom(function(z) { return Math.min(3, z + 0.2); }); }, style: toolbarBtnStyle }, '+'),
478
+ h('div', { style: { color: 'rgba(255,255,255,0.5)', fontSize: 12, minWidth: 40, textAlign: 'center' } }, Math.round(zoom * 100) + '%'),
479
+ h('button', { onClick: function() { setZoom(function(z) { return Math.max(0.15, z - 0.2); }); }, style: toolbarBtnStyle }, '\u2212'),
480
+ h('button', { onClick: fitToView, style: Object.assign({}, toolbarBtnStyle, { fontSize: 11, padding: '4px 10px' }) }, 'Fit'),
481
+ h('button', { onClick: loadData, style: Object.assign({}, toolbarBtnStyle, { fontSize: 11, padding: '4px 10px' }) }, 'Refresh'),
482
+ ),
483
+
484
+ // Canvas
485
+ h('div', {
486
+ ref: containerRef,
487
+ style: { flex: 1, overflow: 'hidden', cursor: dragging ? 'grabbing' : 'grab', position: 'relative' },
488
+ onMouseDown: handleMouseDown,
489
+ onWheel: handleWheel,
490
+ },
491
+ h('div', { style: { transform: 'translate(' + pan.x + 'px, ' + pan.y + 'px) scale(' + zoom + ')', transformOrigin: '0 0', position: 'absolute', top: 0, left: 0 } },
492
+ // SVG edges
493
+ h('svg', { width: treeW, height: treeH, style: { position: 'absolute', top: 0, left: 0, pointerEvents: 'none' } },
494
+ h('defs', null,
495
+ h('marker', { id: 'task-arrow', markerWidth: 8, markerHeight: 6, refX: 8, refY: 3, orient: 'auto' },
496
+ h('polygon', { points: '0 0, 8 3, 0 6', fill: EDGE_COLOR })
497
+ ),
498
+ h('marker', { id: 'task-arrow-hl', markerWidth: 8, markerHeight: 6, refX: 8, refY: 3, orient: 'auto' },
499
+ h('polygon', { points: '0 0, 8 3, 0 6', fill: EDGE_HIGHLIGHT })
500
+ )
501
+ ),
502
+ edges.map(function(e, i) {
503
+ var pId = e.parent.id || e.parent.agentId;
504
+ var cId = e.child.id || e.child.agentId;
505
+ var isHl = connected && connected.has(pId) && connected.has(cId);
506
+ var dim = connected && !isHl;
507
+ return h('path', {
508
+ key: i,
509
+ d: edgePath(e.parent, e.child),
510
+ stroke: isHl ? EDGE_HIGHLIGHT : dim ? 'rgba(255,255,255,0.06)' : EDGE_COLOR,
511
+ strokeWidth: isHl ? 2.5 : 1.5,
512
+ fill: 'none',
513
+ markerEnd: isHl ? 'url(#task-arrow-hl)' : 'url(#task-arrow)',
514
+ style: { transition: 'stroke 0.2s, opacity 0.2s', opacity: dim ? 0.3 : 1 },
515
+ });
516
+ })
517
+ ),
518
+
519
+ // Nodes
520
+ positioned.map(function(node) {
521
+ var nodeId = node.id || node.agentId;
522
+ var isHovered = hoveredId === nodeId;
523
+ var dim = connected && !connected.has(nodeId);
524
+
525
+ if (node.isAgent) {
526
+ // Agent node (top-level, like org-chart node)
527
+ var taskCount = (node.children || []).length;
528
+ return h('div', {
529
+ key: nodeId,
530
+ className: 'task-node',
531
+ onMouseEnter: function(ev) { setHoveredId(nodeId); setMousePos({ x: ev.clientX, y: ev.clientY }); },
532
+ onMouseMove: function(ev) { if (isHovered) setMousePos({ x: ev.clientX, y: ev.clientY }); },
533
+ onMouseLeave: function() { setHoveredId(null); },
534
+ style: {
535
+ position: 'absolute', left: node.x, top: node.y, width: NODE_W, height: NODE_H,
536
+ background: isHovered ? 'rgba(99,102,241,0.12)' : 'rgba(255,255,255,0.03)',
537
+ border: '1.5px solid ' + (isHovered ? 'rgba(99,102,241,0.5)' : 'rgba(255,255,255,0.12)'),
538
+ borderRadius: 12, padding: '10px 14px', cursor: 'default',
539
+ transition: 'all 0.2s', opacity: dim ? 0.2 : 1,
540
+ backdropFilter: 'blur(8px)',
541
+ display: 'flex', alignItems: 'center', gap: 10, userSelect: 'none',
542
+ },
543
+ },
544
+ h('div', { style: { width: 36, height: 36, borderRadius: '50%', background: 'linear-gradient(135deg, ' + ACCENT + '44, ' + ACCENT + '11)', border: '2px solid ' + ACCENT, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 14, fontWeight: 700, color: ACCENT, flexShrink: 0 } },
545
+ (node.name || '?').charAt(0).toUpperCase()
546
+ ),
547
+ h('div', { style: { overflow: 'hidden', flex: 1, minWidth: 0 } },
548
+ h('div', { style: { fontSize: 13, fontWeight: 600, color: '#fff', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, node.name),
549
+ h('div', { style: { fontSize: 11, color: 'rgba(255,255,255,0.45)', marginTop: 2 } }, 'Agent'),
550
+ h('div', { style: { display: 'flex', gap: 4, marginTop: 4 } },
551
+ h('span', { style: tagStyle(ACCENT) }, taskCount + ' task' + (taskCount !== 1 ? 's' : ''))
552
+ )
553
+ )
554
+ );
555
+ }
556
+
557
+ // Task node
558
+ var t = node.task;
559
+ var statusColor = STATUS_COLORS[t.status] || '#6b7394';
560
+ var priColor = PRIORITY_COLORS[t.priority] || '#6366f1';
561
+ var catIcon = CATEGORY_ICONS[t.category] || '\uD83D\uDCCB';
562
+
563
+ return h('div', {
564
+ key: nodeId,
565
+ className: 'task-node',
566
+ onClick: function() { setSelectedTask(t); },
567
+ onMouseEnter: function(ev) { setHoveredId(nodeId); setMousePos({ x: ev.clientX, y: ev.clientY }); },
568
+ onMouseMove: function(ev) { if (isHovered) setMousePos({ x: ev.clientX, y: ev.clientY }); },
569
+ onMouseLeave: function() { setHoveredId(null); },
570
+ style: {
571
+ position: 'absolute', left: node.x, top: node.y, width: NODE_W, height: NODE_H,
572
+ background: isHovered ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.02)',
573
+ border: '1.5px solid ' + (isHovered ? statusColor + '88' : 'rgba(255,255,255,0.12)'),
574
+ borderLeft: '3px solid ' + statusColor,
575
+ borderRadius: 12, padding: '8px 12px', cursor: 'pointer',
576
+ transition: 'all 0.2s', opacity: dim ? 0.2 : 1,
577
+ backdropFilter: 'blur(8px)',
578
+ display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 4, userSelect: 'none',
579
+ },
580
+ },
581
+ // Title row
582
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
583
+ h('span', { style: { fontSize: 12 } }, catIcon),
584
+ h('span', { style: { fontSize: 12, fontWeight: 600, color: '#fff', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, t.title)
585
+ ),
586
+ // Status + priority + time
587
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
588
+ h('span', { style: tagStyle(statusColor) }, t.status.replace('_', ' ')),
589
+ h('span', { style: tagStyle(priColor) }, t.priority),
590
+ h('span', { style: { fontSize: 10, color: 'rgba(255,255,255,0.35)', marginLeft: 'auto' } }, timeAgo(t.createdAt))
591
+ ),
592
+ // Progress bar
593
+ t.status === 'in_progress' && t.progress > 0 && h('div', { style: { height: 2, background: 'rgba(255,255,255,0.1)', borderRadius: 1, overflow: 'hidden', marginTop: 2 } },
594
+ h('div', { style: { height: '100%', width: t.progress + '%', background: STATUS_COLORS.in_progress, borderRadius: 1 } })
595
+ )
596
+ );
597
+ })
598
+ )
599
+ ),
600
+
601
+ // Hover tooltip
602
+ hoveredNode && !hoveredNode.isAgent && hoveredNode.task && h('div', { style: {
603
+ position: 'fixed', left: mousePos.x + 16, top: mousePos.y - 10,
604
+ background: 'rgba(15,17,23,0.95)', backdropFilter: 'blur(12px)',
605
+ border: '1px solid rgba(255,255,255,0.15)', borderRadius: 10,
606
+ padding: '12px 16px', pointerEvents: 'none', zIndex: 1000, minWidth: 200, maxWidth: 300,
607
+ }},
608
+ h('div', { style: { fontSize: 13, fontWeight: 600, color: '#fff', marginBottom: 8 } }, hoveredNode.task.title),
609
+ h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
610
+ tooltipRow('Status', hoveredNode.task.status.replace('_', ' '), STATUS_COLORS[hoveredNode.task.status]),
611
+ tooltipRow('Priority', hoveredNode.task.priority, PRIORITY_COLORS[hoveredNode.task.priority]),
612
+ tooltipRow('Agent', hoveredNode.task.assignedToName || '-'),
613
+ tooltipRow('Category', hoveredNode.task.category),
614
+ hoveredNode.task.modelUsed && tooltipRow('Model', hoveredNode.task.modelUsed),
615
+ hoveredNode.task.startedAt && tooltipRow('Started', timeAgo(hoveredNode.task.startedAt)),
616
+ hoveredNode.task.actualDurationMs && tooltipRow('Duration', formatDuration(hoveredNode.task.actualDurationMs)),
617
+ hoveredNode.task.progress > 0 && tooltipRow('Progress', hoveredNode.task.progress + '%', STATUS_COLORS.in_progress)
618
+ )
619
+ ),
620
+
621
+ // Agent hover tooltip
622
+ hoveredNode && hoveredNode.isAgent && h('div', { style: {
623
+ position: 'fixed', left: mousePos.x + 16, top: mousePos.y - 10,
624
+ background: 'rgba(15,17,23,0.95)', backdropFilter: 'blur(12px)',
625
+ border: '1px solid rgba(255,255,255,0.15)', borderRadius: 10,
626
+ padding: '12px 16px', pointerEvents: 'none', zIndex: 1000, minWidth: 180,
627
+ }},
628
+ h('div', { style: { fontSize: 13, fontWeight: 600, color: '#fff', marginBottom: 6 } }, hoveredNode.name),
629
+ tooltipRow('Tasks', String((hoveredNode.children || []).length), ACCENT)
630
+ ),
631
+
632
+ // Task detail modal
633
+ selectedTask && h(TaskDetail, { task: selectedTask, onClose: function() { setSelectedTask(null); }, onCancel: cancelTask })
634
+ );
635
+ }
636
+
637
+ // ─── Agent Task Pipeline (for agent-detail workforce tab) ─
638
+ // Reusable mini version scoped to a single agent
639
+ export function AgentTaskPipeline(props) {
640
+ var agentId = props.agentId;
641
+ var _tasks = useState([]);
642
+ var tasks = _tasks[0]; var setTasks = _tasks[1];
643
+ var _loading = useState(true);
644
+ var loading = _loading[0]; var setLoading = _loading[1];
645
+ var _selectedTask = useState(null);
646
+ var selectedTask = _selectedTask[0]; var setSelectedTask = _selectedTask[1];
647
+ var app = useApp();
648
+ var toast = app.toast;
649
+
650
+ useEffect(function() {
651
+ setLoading(true);
652
+ engineCall('/task-pipeline/agent/' + agentId + '?completed=true').then(function(res) {
653
+ setTasks(res?.tasks || []);
654
+ }).catch(function() {}).finally(function() { setLoading(false); });
655
+
656
+ // SSE
657
+ var baseUrl = window.__ENGINE_BASE || '/api/engine';
658
+ var es;
659
+ try {
660
+ es = new EventSource(baseUrl + '/task-pipeline/stream');
661
+ es.onmessage = function(e) {
662
+ try {
663
+ var event = JSON.parse(e.data);
664
+ if (event.task && event.task.assignedTo === agentId) {
665
+ setTasks(function(prev) {
666
+ var idx = prev.findIndex(function(t) { return t.id === event.task.id; });
667
+ if (idx >= 0) { var next = prev.slice(); next[idx] = event.task; return next; }
668
+ return [event.task].concat(prev);
669
+ });
670
+ }
671
+ } catch (err) {}
672
+ };
673
+ } catch (err) {}
674
+ return function() { if (es) es.close(); };
675
+ }, [agentId]);
676
+
677
+ var cancelTask = useCallback(function(taskId) {
678
+ engineCall('/task-pipeline/' + taskId + '/cancel', { method: 'POST' }).then(function() {
679
+ toast('Task cancelled', 'success');
680
+ setSelectedTask(null);
681
+ }).catch(function(err) { toast(err.message, 'error'); });
682
+ }, []);
683
+
684
+ if (loading) return h('div', { style: { padding: 20, textAlign: 'center', color: 'var(--text-muted)', fontSize: 13 } }, 'Loading tasks...');
685
+ if (!tasks.length) return h('div', { style: { padding: 20, textAlign: 'center', color: 'var(--text-muted)', fontSize: 13 } }, 'No pipeline tasks for this agent yet.');
686
+
687
+ var active = tasks.filter(function(t) { return t.status === 'created' || t.status === 'assigned' || t.status === 'in_progress'; });
688
+ var completed = tasks.filter(function(t) { return t.status === 'completed'; });
689
+ var failed = tasks.filter(function(t) { return t.status === 'failed' || t.status === 'cancelled'; });
690
+
691
+ function renderTaskRow(t) {
692
+ var statusColor = STATUS_COLORS[t.status] || '#6b7394';
693
+ var catIcon = CATEGORY_ICONS[t.category] || '\uD83D\uDCCB';
694
+ return h('div', {
695
+ key: t.id,
696
+ onClick: function() { setSelectedTask(t); },
697
+ style: {
698
+ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px',
699
+ borderBottom: '1px solid var(--border)', cursor: 'pointer',
700
+ transition: 'background 0.15s',
701
+ },
702
+ onMouseEnter: function(e) { e.currentTarget.style.background = 'var(--bg-secondary)'; },
703
+ onMouseLeave: function(e) { e.currentTarget.style.background = ''; },
704
+ },
705
+ h('span', { style: { fontSize: 14, flexShrink: 0 } }, catIcon),
706
+ h('div', { style: { flex: 1, minWidth: 0 } },
707
+ h('div', { style: { fontSize: 13, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, t.title),
708
+ h('div', { style: { fontSize: 11, color: 'var(--text-muted)', marginTop: 2 } }, t.category + ' \u00B7 ' + timeAgo(t.createdAt))
709
+ ),
710
+ t.status === 'in_progress' && t.progress > 0 && h('div', { style: { width: 40, fontSize: 11, color: STATUS_COLORS.in_progress, fontWeight: 600 } }, t.progress + '%'),
711
+ h('span', { style: { padding: '2px 8px', borderRadius: 10, fontSize: 10, fontWeight: 600, background: statusColor + '22', color: statusColor, flexShrink: 0 } }, t.status.replace('_', ' ')),
712
+ t.actualDurationMs && h('span', { style: { fontSize: 11, color: 'var(--text-muted)', flexShrink: 0 } }, formatDuration(t.actualDurationMs))
713
+ );
714
+ }
715
+
716
+ return h(Fragment, null,
717
+ // Active
718
+ active.length > 0 && h('div', { style: { marginBottom: 16 } },
719
+ h('div', { style: { fontSize: 12, fontWeight: 600, color: STATUS_COLORS.in_progress, marginBottom: 8, display: 'flex', alignItems: 'center', gap: 6 } },
720
+ h('div', { style: { width: 6, height: 6, borderRadius: '50%', background: STATUS_COLORS.in_progress, animation: 'pulse 2s infinite' } }),
721
+ 'Active (' + active.length + ')'
722
+ ),
723
+ h('div', { style: { border: '1px solid var(--border)', borderRadius: 'var(--radius)', overflow: 'hidden' } },
724
+ active.map(renderTaskRow)
725
+ )
726
+ ),
727
+ // Completed
728
+ completed.length > 0 && h('div', { style: { marginBottom: 16 } },
729
+ h('div', { style: { fontSize: 12, fontWeight: 600, color: STATUS_COLORS.completed, marginBottom: 8 } }, 'Completed (' + completed.length + ')'),
730
+ h('div', { style: { border: '1px solid var(--border)', borderRadius: 'var(--radius)', overflow: 'hidden' } },
731
+ completed.slice(0, 10).map(renderTaskRow)
732
+ ),
733
+ completed.length > 10 && h('div', { style: { padding: 8, textAlign: 'center', fontSize: 12, color: 'var(--text-muted)' } }, '+ ' + (completed.length - 10) + ' more')
734
+ ),
735
+ // Failed
736
+ failed.length > 0 && h('div', { style: { marginBottom: 16 } },
737
+ h('div', { style: { fontSize: 12, fontWeight: 600, color: STATUS_COLORS.failed, marginBottom: 8 } }, 'Failed (' + failed.length + ')'),
738
+ h('div', { style: { border: '1px solid var(--border)', borderRadius: 'var(--radius)', overflow: 'hidden' } },
739
+ failed.slice(0, 5).map(renderTaskRow)
740
+ )
741
+ ),
742
+ // Detail modal
743
+ selectedTask && h(TaskDetail, { task: selectedTask, onClose: function() { setSelectedTask(null); }, onCancel: cancelTask })
744
+ );
745
+ }