@agenticmail/enterprise 0.5.201 → 0.5.203

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.
@@ -2,6 +2,13 @@ import { h, useState, useEffect, useCallback, useRef, Fragment, useApp, engineCa
2
2
  import { I } from '../components/icons.js';
3
3
  import { HelpButton } from '../components/help-button.js';
4
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
+
5
12
  // ─── Colors ──────────────────────────────────────────────
6
13
  const STATUS_COLORS = {
7
14
  created: '#6366f1',
@@ -12,34 +19,115 @@ const STATUS_COLORS = {
12
19
  cancelled: '#6b7394',
13
20
  };
14
21
  const PRIORITY_COLORS = { urgent: '#ef4444', high: '#f59e0b', normal: '#6366f1', low: '#6b7394' };
15
- const 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' };
16
- const BG = '#0a0c14';
17
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';
18
26
 
19
- // ─── Styles ──────────────────────────────────────────────
20
- const _h4 = { marginTop: 16, marginBottom: 8, fontSize: 14 };
21
- const _ul = { paddingLeft: 20, margin: '4px 0 8px' };
22
- const _tip = { marginTop: 12, padding: 12, background: 'var(--bg-secondary, #1e293b)', borderRadius: 'var(--radius, 8px)', fontSize: 13 };
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
+ });
23
40
 
24
- // ─── Pipeline Columns (Kanban) ───────────────────────────
25
- const COLUMNS = [
26
- { key: 'created', label: 'Queued', color: STATUS_COLORS.created },
27
- { key: 'assigned', label: 'Assigned', color: STATUS_COLORS.assigned },
28
- { key: 'in_progress', label: 'In Progress', color: STATUS_COLORS.in_progress },
29
- { key: 'completed', label: 'Completed', color: STATUS_COLORS.completed },
30
- { key: 'failed', label: 'Failed', color: STATUS_COLORS.failed },
31
- ];
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
+ });
32
67
 
33
- function formatDuration(ms) {
34
- if (!ms) return '-';
35
- var s = Math.floor(ms / 1000);
36
- if (s < 60) return s + 's';
37
- var m = Math.floor(s / 60);
38
- if (m < 60) return m + 'm ' + (s % 60) + 's';
39
- var hr = Math.floor(m / 60);
40
- return hr + 'h ' + (m % 60) + 'm';
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;
41
128
  }
42
129
 
130
+ // ─── Helpers ─────────────────────────────────────────────
43
131
  function timeAgo(ts) {
44
132
  if (!ts) return '-';
45
133
  var diff = Date.now() - new Date(ts).getTime();
@@ -50,51 +138,57 @@ function timeAgo(ts) {
50
138
  return Math.floor(diff / 86400000) + 'd ago';
51
139
  }
52
140
 
53
- // ─── Task Card Component ─────────────────────────────────
54
- function TaskCard({ task, onSelect }) {
55
- var priColor = PRIORITY_COLORS[task.priority] || PRIORITY_COLORS.normal;
56
- var catIcon = CATEGORY_ICONS[task.category] || CATEGORY_ICONS.custom;
57
-
58
- return h('div', {
59
- onClick: function() { onSelect(task); },
60
- style: {
61
- background: 'var(--bg-secondary, #111827)',
62
- border: '1px solid var(--border, #1e293b)',
63
- borderLeft: '3px solid ' + priColor,
64
- borderRadius: 'var(--radius, 8px)',
65
- padding: 12,
66
- marginBottom: 8,
67
- cursor: 'pointer',
68
- transition: 'transform 0.1s, box-shadow 0.1s',
69
- },
70
- onMouseEnter: function(e) { e.currentTarget.style.transform = 'translateY(-1px)'; e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)'; },
71
- onMouseLeave: function(e) { e.currentTarget.style.transform = ''; e.currentTarget.style.boxShadow = ''; },
72
- },
73
- // Header: icon + title
74
- h('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 } },
75
- h('span', { style: { fontSize: 14 } }, catIcon),
76
- h('span', { style: { fontSize: 13, fontWeight: 600, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, task.title)
77
- ),
78
- // Agent + time
79
- h('div', { style: { display: 'flex', justifyContent: 'space-between', fontSize: 11, color: 'var(--text-muted)' } },
80
- h('span', null, task.assignedToName || task.assignedTo || '-'),
81
- h('span', null, timeAgo(task.createdAt))
82
- ),
83
- // Progress bar (if in progress)
84
- task.status === 'in_progress' && task.progress > 0 && h('div', { style: { marginTop: 6, height: 3, background: 'var(--border)', borderRadius: 2, overflow: 'hidden' } },
85
- h('div', { style: { height: '100%', width: task.progress + '%', background: STATUS_COLORS.in_progress, borderRadius: 2, transition: 'width 0.3s' } })
86
- ),
87
- // Tags
88
- task.tags && task.tags.length > 0 && h('div', { style: { display: 'flex', gap: 4, marginTop: 6, flexWrap: 'wrap' } },
89
- task.tags.map(function(tag) {
90
- return h('span', { key: tag, style: { fontSize: 10, padding: '1px 6px', borderRadius: 10, background: 'var(--accent-soft, rgba(99,102,241,0.15))', color: 'var(--accent, #6366f1)' } }, tag);
91
- })
92
- )
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)
93
172
  );
94
173
  }
95
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
+
96
187
  // ─── Task Detail Modal ───────────────────────────────────
97
- function TaskDetail({ task, onClose, onCancel }) {
188
+ function TaskDetail(props) {
189
+ var task = props.task;
190
+ var onClose = props.onClose;
191
+ var onCancel = props.onCancel;
98
192
  if (!task) return null;
99
193
  var statusColor = STATUS_COLORS[task.status] || '#6b7394';
100
194
 
@@ -105,34 +199,18 @@ function TaskDetail({ task, onClose, onCancel }) {
105
199
  h('button', { className: 'btn btn-ghost btn-icon', onClick: onClose }, '\u00D7')
106
200
  ),
107
201
  h('div', { className: 'modal-body', style: { padding: 20 } },
108
- // Status badge
109
- h('div', { style: { display: 'flex', gap: 8, marginBottom: 16, alignItems: 'center' } },
110
- h('span', { style: { padding: '3px 10px', borderRadius: 12, fontSize: 12, fontWeight: 600, background: statusColor + '22', color: statusColor, border: '1px solid ' + statusColor + '44' } },
111
- task.status.replace('_', ' ').toUpperCase()
112
- ),
113
- h('span', { style: { padding: '3px 10px', borderRadius: 12, fontSize: 12, background: (PRIORITY_COLORS[task.priority] || '#6366f1') + '22', color: PRIORITY_COLORS[task.priority] || '#6366f1' } },
114
- task.priority.toUpperCase()
115
- ),
116
- h('span', { style: { padding: '3px 10px', borderRadius: 12, fontSize: 12, background: 'var(--bg-tertiary)', color: 'var(--text-muted)' } },
117
- (CATEGORY_ICONS[task.category] || '') + ' ' + task.category
118
- )
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)
119
206
  ),
120
-
121
- // Description
122
207
  task.description && h('div', { style: { marginBottom: 16, fontSize: 13, lineHeight: 1.6, color: 'var(--text-secondary)' } }, task.description),
123
-
124
- // Progress
125
208
  task.status === 'in_progress' && h('div', { style: { marginBottom: 16 } },
126
- h('div', { style: { display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 4 } },
127
- h('span', null, 'Progress'),
128
- h('span', null, task.progress + '%')
129
- ),
209
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', fontSize: 12, marginBottom: 4 } }, h('span', null, 'Progress'), h('span', null, task.progress + '%')),
130
210
  h('div', { style: { height: 6, background: 'var(--border)', borderRadius: 3, overflow: 'hidden' } },
131
211
  h('div', { style: { height: '100%', width: task.progress + '%', background: STATUS_COLORS.in_progress, borderRadius: 3 } })
132
212
  )
133
213
  ),
134
-
135
- // Metadata grid
136
214
  h('div', { style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px 24px', fontSize: 13, marginBottom: 16 } },
137
215
  h('div', null, h('div', { style: { color: 'var(--text-muted)', fontSize: 11, marginBottom: 2 } }, 'Assigned To'), h('div', null, task.assignedToName || task.assignedTo || '-')),
138
216
  h('div', null, h('div', { style: { color: 'var(--text-muted)', fontSize: 11, marginBottom: 2 } }, 'Created By'), h('div', null, task.createdByName || task.createdBy || '-')),
@@ -143,21 +221,11 @@ function TaskDetail({ task, onClose, onCancel }) {
143
221
  h('div', null, h('div', { style: { color: 'var(--text-muted)', fontSize: 11, marginBottom: 2 } }, 'Model'), h('div', null, task.modelUsed || task.model || '-')),
144
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)))
145
223
  ),
146
-
147
- // Error
148
- 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' } },
149
- h('strong', null, 'Error: '), task.error
150
- ),
151
-
152
- // Result
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),
153
225
  task.result && h('div', { style: { marginBottom: 16 } },
154
226
  h('div', { style: { fontSize: 12, color: 'var(--text-muted)', marginBottom: 4 } }, 'Result'),
155
- h('pre', { style: { fontSize: 12, background: 'var(--bg-tertiary)', padding: 12, borderRadius: 'var(--radius)', overflow: 'auto', maxHeight: 200 } },
156
- JSON.stringify(task.result, null, 2)
157
- )
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))
158
228
  ),
159
-
160
- // Actions
161
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 } },
162
230
  h('button', { className: 'btn btn-danger btn-sm', onClick: function() { onCancel(task.id); } }, 'Cancel Task')
163
231
  )
@@ -166,153 +234,189 @@ function TaskDetail({ task, onClose, onCancel }) {
166
234
  );
167
235
  }
168
236
 
169
- // ─── Stats Bar ───────────────────────────────────────────
170
- function StatsBar({ stats }) {
171
- var items = [
172
- { label: 'Queued', value: stats.created, color: STATUS_COLORS.created },
173
- { label: 'Assigned', value: stats.assigned, color: STATUS_COLORS.assigned },
174
- { label: 'In Progress', value: stats.inProgress, color: STATUS_COLORS.in_progress },
175
- { label: 'Completed', value: stats.completed, color: STATUS_COLORS.completed },
176
- { label: 'Failed', value: stats.failed, color: STATUS_COLORS.failed },
177
- ];
178
- return h('div', { style: { display: 'flex', gap: 16, marginBottom: 20, flexWrap: 'wrap' } },
179
- items.map(function(it) {
180
- return h('div', { key: it.label, style: { background: 'var(--bg-secondary)', border: '1px solid var(--border)', borderRadius: 'var(--radius)', padding: '12px 20px', minWidth: 120, textAlign: 'center' } },
181
- h('div', { style: { fontSize: 28, fontWeight: 700, color: it.color, lineHeight: 1 } }, it.value),
182
- h('div', { style: { fontSize: 11, color: 'var(--text-muted)', marginTop: 4 } }, it.label)
183
- );
184
- })
185
- );
186
- }
187
-
188
- // ─── Main Page Component ─────────────────────────────────
237
+ // ─── Main Page ───────────────────────────────────────────
189
238
  export function TaskPipelinePage() {
190
- var { toast } = useApp();
191
- var [tasks, setTasks] = useState([]);
192
- var [stats, setStats] = useState({ created: 0, assigned: 0, inProgress: 0, completed: 0, failed: 0, cancelled: 0, total: 0 });
193
- var [selectedTask, setSelectedTask] = useState(null);
194
- var [view, setView] = useState('kanban'); // 'kanban' | 'list' | 'timeline'
195
- var [filter, setFilter] = useState('all'); // 'all' | 'active' | 'completed' | 'failed'
196
- var [loading, setLoading] = useState(true);
197
- var eventSourceRef = useRef(null);
198
-
199
- // Fetch initial data
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
+
200
265
  var loadData = useCallback(function() {
201
266
  setLoading(true);
202
267
  Promise.all([
203
268
  engineCall('/task-pipeline?limit=200'),
204
269
  engineCall('/task-pipeline/stats'),
205
- ]).then(function(results) {
206
- setTasks(results[0]?.tasks || []);
207
- setStats(results[1] || stats);
270
+ ]).then(function(res) {
271
+ setTasks(res[0]?.tasks || []);
272
+ setStats(res[1] || stats);
208
273
  }).catch(function(err) {
209
- console.error('[TaskPipeline] load error:', err);
274
+ console.error('[TaskPipeline] load:', err);
210
275
  }).finally(function() { setLoading(false); });
211
276
  }, []);
212
277
 
213
- // SSE for real-time updates
278
+ // SSE
214
279
  useEffect(function() {
215
280
  loadData();
216
-
217
- // Connect to SSE stream
218
- var baseUrl = window.__ENGINE_BASE || '/engine';
219
- var es = new EventSource(baseUrl + '/task-pipeline/stream');
220
- eventSourceRef.current = es;
221
-
222
- es.onmessage = function(e) {
223
- try {
224
- var event = JSON.parse(e.data);
225
- if (event.type === 'init') {
226
- // Initial state from SSE
227
- if (event.tasks) setTasks(function(prev) {
228
- var map = new Map();
229
- prev.forEach(function(t) { map.set(t.id, t); });
230
- event.tasks.forEach(function(t) { map.set(t.id, t); });
231
- return Array.from(map.values()).sort(function(a, b) { return new Date(b.createdAt) - new Date(a.createdAt); });
232
- });
233
- if (event.stats) setStats(event.stats);
234
- } else if (event.task) {
235
- // Real-time task event
236
- setTasks(function(prev) {
237
- var idx = prev.findIndex(function(t) { return t.id === event.task.id; });
238
- if (idx >= 0) {
239
- var next = prev.slice();
240
- next[idx] = event.task;
241
- return next;
242
- }
243
- return [event.task].concat(prev);
244
- });
245
- // Update stats locally
246
- setStats(function(prev) {
247
- var s = Object.assign({}, prev);
248
- if (event.type === 'task_created') { s.created++; s.total++; }
249
- else if (event.type === 'task_completed') { s.completed++; s.inProgress = Math.max(0, s.inProgress - 1); }
250
- else if (event.type === 'task_failed') { s.failed++; s.inProgress = Math.max(0, s.inProgress - 1); }
251
- return s;
252
- });
253
- // Update selected task if viewing it
254
- setSelectedTask(function(prev) {
255
- if (prev && prev.id === event.task.id) return event.task;
256
- return prev;
257
- });
258
- }
259
- } catch (err) { /* ignore parse errors */ }
260
- };
261
-
262
- es.onerror = function() {
263
- // Reconnect handled by browser
264
- };
265
-
266
- return function() {
267
- if (es) es.close();
268
- };
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(); };
269
311
  }, []);
270
312
 
271
- // Periodic stats refresh
313
+ // Periodic stats
272
314
  useEffect(function() {
273
- var interval = setInterval(function() {
315
+ var iv = setInterval(function() {
274
316
  engineCall('/task-pipeline/stats').then(function(s) { if (s) setStats(s); }).catch(function() {});
275
317
  }, 30000);
276
- return function() { clearInterval(interval); };
318
+ return function() { clearInterval(iv); };
277
319
  }, []);
278
320
 
279
321
  var cancelTask = useCallback(function(taskId) {
280
- engineCall('/task-pipeline/' + taskId + '/cancel', { method: 'POST' })
281
- .then(function(res) {
282
- toast('Task cancelled', 'success');
283
- setSelectedTask(null);
284
- loadData();
285
- })
286
- .catch(function(err) { toast(err.message || 'Failed to cancel', 'error'); });
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'); });
287
327
  }, []);
288
328
 
289
- // Filter tasks
290
- var filteredTasks = tasks.filter(function(t) {
329
+ // Filter
330
+ var filtered = tasks.filter(function(t) {
291
331
  if (filter === 'active') return t.status === 'created' || t.status === 'assigned' || t.status === 'in_progress';
292
332
  if (filter === 'completed') return t.status === 'completed';
293
333
  if (filter === 'failed') return t.status === 'failed' || t.status === 'cancelled';
294
334
  return true;
295
335
  });
296
336
 
297
- // Group by status for kanban
298
- var columns = {};
299
- COLUMNS.forEach(function(col) { columns[col.key] = []; });
300
- filteredTasks.forEach(function(t) {
301
- if (columns[t.status]) columns[t.status].push(t);
302
- else if (t.status === 'cancelled' && columns.failed) columns.failed.push(t);
303
- });
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
+ }, []);
304
350
 
305
- if (loading) {
306
- return h('div', { style: { padding: 40, textAlign: 'center', color: 'var(--text-muted)' } }, 'Loading task pipeline...');
307
- }
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]);
308
358
 
309
- return h(Fragment, null,
310
- // ─── Header ────────────────────────────────────────
311
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 } },
312
- h('div', { style: { display: 'flex', alignItems: 'center', gap: 12 } },
313
- h('h1', { style: { fontSize: 22, fontWeight: 700 } }, I.workflow(), ' Task Pipeline'),
314
- h(HelpButton, { title: 'Task Pipeline' },
315
- h('p', null, 'The centralized task pipeline shows every task assigned to agents across your organization in real-time.'),
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
+ h(HelpButton, { label: 'Task Pipeline' },
419
+ 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.'),
316
420
  h('h4', { style: _h4 }, 'How It Works'),
317
421
  h('ul', { style: _ul },
318
422
  h('li', null, 'Tasks are automatically recorded when agents are spawned for work'),
@@ -322,134 +426,340 @@ export function TaskPipelinePage() {
322
426
  ),
323
427
  h('h4', { style: _h4 }, 'Task Lifecycle'),
324
428
  h('ul', { style: _ul },
325
- h('li', null, h('strong', null, 'Queued'), ' — Task created, waiting to be assigned'),
429
+ h('li', null, h('strong', null, 'Queued'), ' — Waiting to be picked up'),
326
430
  h('li', null, h('strong', null, 'Assigned'), ' — Agent selected, about to start'),
327
- h('li', null, h('strong', null, 'In Progress'), ' — Agent actively working'),
328
- h('li', null, h('strong', null, 'Completed'), ' — Task finished successfully'),
329
- h('li', null, h('strong', null, 'Failed'), ' — Task encountered an error')
431
+ h('li', null, h('strong', null, 'In Progress'), ' — Actively executing'),
432
+ h('li', null, h('strong', null, 'Completed'), ' — Finished successfully'),
433
+ h('li', null, h('strong', null, 'Failed'), ' — Encountered an error')
330
434
  ),
331
- h('div', { style: _tip }, 'Tip: Click any task card to see full details including model used, tokens consumed, duration, and results.')
435
+ h('div', { style: _tip }, 'Tip: Tasks will appear here as agents are assigned work. Sub-tasks show as children connected by arrows.')
332
436
  )
333
437
  ),
334
- h('div', { style: { display: 'flex', gap: 8 } },
335
- // View toggle
336
- h('div', { style: { display: 'flex', border: '1px solid var(--border)', borderRadius: 'var(--radius)', overflow: 'hidden' } },
337
- ['kanban', 'list'].map(function(v) {
338
- return h('button', {
339
- key: v,
340
- className: 'btn btn-sm ' + (view === v ? 'btn-primary' : 'btn-ghost'),
341
- onClick: function() { setView(v); },
342
- style: { borderRadius: 0, textTransform: 'capitalize', fontSize: 12 }
343
- }, v);
438
+ ),
439
+ h('div', { style: { flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' } },
440
+ h('div', { style: { fontSize: 48, marginBottom: 16 } }, '\uD83D\uDCCB'),
441
+ h('div', { style: { fontSize: 18, fontWeight: 600, marginBottom: 8, color: '#fff' } }, 'No Tasks in Pipeline'),
442
+ h('div', { style: { color: 'rgba(255,255,255,0.5)' } }, 'Tasks will appear here as agents are assigned work.')
443
+ )
444
+ );
445
+
446
+ return h('div', { style: { height: '100%', display: 'flex', flexDirection: 'column', background: BG, borderRadius: 'var(--radius-lg)', overflow: 'hidden' } },
447
+ // Toolbar
448
+ 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' } },
449
+ h('div', { style: { fontWeight: 700, fontSize: 16, color: '#fff', display: 'flex', alignItems: 'center', gap: 8 } },
450
+ I.workflow(), ' Task Pipeline',
451
+ h(HelpButton, { label: 'Task Pipeline' },
452
+ 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.'),
453
+ h('h4', { style: _h4 }, 'Interactions'),
454
+ h('ul', { style: _ul },
455
+ h('li', null, h('strong', null, 'Hover'), ' — Highlights the task chain and shows detail tooltip'),
456
+ h('li', null, h('strong', null, 'Click'), ' — Opens full task detail modal'),
457
+ h('li', null, h('strong', null, 'Scroll'), ' — Zoom in/out'),
458
+ h('li', null, h('strong', null, 'Drag'), ' — Pan the canvas')
459
+ ),
460
+ h('h4', { style: _h4 }, 'Task Lifecycle'),
461
+ h('ul', { style: _ul },
462
+ h('li', null, h('strong', null, 'Queued'), ' — Waiting to be picked up'),
463
+ h('li', null, h('strong', null, 'Assigned'), ' — Agent selected, about to start'),
464
+ h('li', null, h('strong', null, 'In Progress'), ' — Actively executing'),
465
+ h('li', null, h('strong', null, 'Completed'), ' — Finished successfully'),
466
+ h('li', null, h('strong', null, 'Failed'), ' — Encountered an error')
467
+ ),
468
+ h('div', { style: _tip }, 'Tip: Sub-tasks appear as children of their parent task. The arrow system shows task delegation flow.')
469
+ )
470
+ ),
471
+ // Live dot
472
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 4 } },
473
+ h('div', { style: { width: 8, height: 8, borderRadius: '50%', background: '#22c55e', animation: 'pulse 2s infinite' } }),
474
+ h('span', { style: { color: 'rgba(255,255,255,0.4)', fontSize: 12 } }, 'Live')
475
+ ),
476
+ // Stats
477
+ h('div', { style: { color: 'rgba(255,255,255,0.4)', fontSize: 12 } },
478
+ (stats.inProgress || 0) + ' active \u00B7 ' + (stats.completed || 0) + ' done \u00B7 ' + (stats.total || 0) + ' total'
479
+ ),
480
+ h('div', { style: { flex: 1 } }),
481
+ // Filter
482
+ ['active', 'all', 'completed', 'failed'].map(function(f) {
483
+ return h('button', {
484
+ key: f,
485
+ onClick: function() { setFilter(f); },
486
+ 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 })
487
+ }, f.charAt(0).toUpperCase() + f.slice(1));
488
+ }),
489
+ h('div', { style: { width: 1, height: 16, background: 'rgba(255,255,255,0.12)' } }),
490
+ // Legend
491
+ legendDot(STATUS_COLORS.in_progress, 'Active'),
492
+ legendDot(STATUS_COLORS.assigned, 'Assigned'),
493
+ legendDot(STATUS_COLORS.completed, 'Done'),
494
+ legendDot(STATUS_COLORS.failed, 'Failed'),
495
+ h('div', { style: { width: 1, height: 16, background: 'rgba(255,255,255,0.12)' } }),
496
+ // Zoom
497
+ h('button', { onClick: function() { setZoom(function(z) { return Math.min(3, z + 0.2); }); }, style: toolbarBtnStyle }, '+'),
498
+ h('div', { style: { color: 'rgba(255,255,255,0.5)', fontSize: 12, minWidth: 40, textAlign: 'center' } }, Math.round(zoom * 100) + '%'),
499
+ h('button', { onClick: function() { setZoom(function(z) { return Math.max(0.15, z - 0.2); }); }, style: toolbarBtnStyle }, '\u2212'),
500
+ h('button', { onClick: fitToView, style: Object.assign({}, toolbarBtnStyle, { fontSize: 11, padding: '4px 10px' }) }, 'Fit'),
501
+ h('button', { onClick: loadData, style: Object.assign({}, toolbarBtnStyle, { fontSize: 11, padding: '4px 10px' }) }, 'Refresh'),
502
+ ),
503
+
504
+ // Canvas
505
+ h('div', {
506
+ ref: containerRef,
507
+ style: { flex: 1, overflow: 'hidden', cursor: dragging ? 'grabbing' : 'grab', position: 'relative' },
508
+ onMouseDown: handleMouseDown,
509
+ onWheel: handleWheel,
510
+ },
511
+ h('div', { style: { transform: 'translate(' + pan.x + 'px, ' + pan.y + 'px) scale(' + zoom + ')', transformOrigin: '0 0', position: 'absolute', top: 0, left: 0 } },
512
+ // SVG edges
513
+ h('svg', { width: treeW, height: treeH, style: { position: 'absolute', top: 0, left: 0, pointerEvents: 'none' } },
514
+ h('defs', null,
515
+ h('marker', { id: 'task-arrow', markerWidth: 8, markerHeight: 6, refX: 8, refY: 3, orient: 'auto' },
516
+ h('polygon', { points: '0 0, 8 3, 0 6', fill: EDGE_COLOR })
517
+ ),
518
+ h('marker', { id: 'task-arrow-hl', markerWidth: 8, markerHeight: 6, refX: 8, refY: 3, orient: 'auto' },
519
+ h('polygon', { points: '0 0, 8 3, 0 6', fill: EDGE_HIGHLIGHT })
520
+ )
521
+ ),
522
+ edges.map(function(e, i) {
523
+ var pId = e.parent.id || e.parent.agentId;
524
+ var cId = e.child.id || e.child.agentId;
525
+ var isHl = connected && connected.has(pId) && connected.has(cId);
526
+ var dim = connected && !isHl;
527
+ return h('path', {
528
+ key: i,
529
+ d: edgePath(e.parent, e.child),
530
+ stroke: isHl ? EDGE_HIGHLIGHT : dim ? 'rgba(255,255,255,0.06)' : EDGE_COLOR,
531
+ strokeWidth: isHl ? 2.5 : 1.5,
532
+ fill: 'none',
533
+ markerEnd: isHl ? 'url(#task-arrow-hl)' : 'url(#task-arrow)',
534
+ style: { transition: 'stroke 0.2s, opacity 0.2s', opacity: dim ? 0.3 : 1 },
535
+ });
344
536
  })
345
537
  ),
346
- // Filter
347
- h('select', {
348
- className: 'form-control',
349
- value: filter,
350
- onChange: function(e) { setFilter(e.target.value); },
351
- style: { fontSize: 12, padding: '4px 8px', width: 'auto' }
352
- },
353
- h('option', { value: 'all' }, 'All Tasks'),
354
- h('option', { value: 'active' }, 'Active Only'),
355
- h('option', { value: 'completed' }, 'Completed'),
356
- h('option', { value: 'failed' }, 'Failed / Cancelled')
357
- ),
358
- h('button', { className: 'btn btn-secondary btn-sm', onClick: loadData }, I.refresh(), ' Refresh')
538
+
539
+ // Nodes
540
+ positioned.map(function(node) {
541
+ var nodeId = node.id || node.agentId;
542
+ var isHovered = hoveredId === nodeId;
543
+ var dim = connected && !connected.has(nodeId);
544
+
545
+ if (node.isAgent) {
546
+ // Agent node (top-level, like org-chart node)
547
+ var taskCount = (node.children || []).length;
548
+ return h('div', {
549
+ key: nodeId,
550
+ className: 'task-node',
551
+ onMouseEnter: function(ev) { setHoveredId(nodeId); setMousePos({ x: ev.clientX, y: ev.clientY }); },
552
+ onMouseMove: function(ev) { if (isHovered) setMousePos({ x: ev.clientX, y: ev.clientY }); },
553
+ onMouseLeave: function() { setHoveredId(null); },
554
+ style: {
555
+ position: 'absolute', left: node.x, top: node.y, width: NODE_W, height: NODE_H,
556
+ background: isHovered ? 'rgba(99,102,241,0.12)' : 'rgba(255,255,255,0.03)',
557
+ border: '1.5px solid ' + (isHovered ? 'rgba(99,102,241,0.5)' : 'rgba(255,255,255,0.12)'),
558
+ borderRadius: 12, padding: '10px 14px', cursor: 'default',
559
+ transition: 'all 0.2s', opacity: dim ? 0.2 : 1,
560
+ backdropFilter: 'blur(8px)',
561
+ display: 'flex', alignItems: 'center', gap: 10, userSelect: 'none',
562
+ },
563
+ },
564
+ 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 } },
565
+ (node.name || '?').charAt(0).toUpperCase()
566
+ ),
567
+ h('div', { style: { overflow: 'hidden', flex: 1, minWidth: 0 } },
568
+ h('div', { style: { fontSize: 13, fontWeight: 600, color: '#fff', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, node.name),
569
+ h('div', { style: { fontSize: 11, color: 'rgba(255,255,255,0.45)', marginTop: 2 } }, 'Agent'),
570
+ h('div', { style: { display: 'flex', gap: 4, marginTop: 4 } },
571
+ h('span', { style: tagStyle(ACCENT) }, taskCount + ' task' + (taskCount !== 1 ? 's' : ''))
572
+ )
573
+ )
574
+ );
575
+ }
576
+
577
+ // Task node
578
+ var t = node.task;
579
+ var statusColor = STATUS_COLORS[t.status] || '#6b7394';
580
+ var priColor = PRIORITY_COLORS[t.priority] || '#6366f1';
581
+ var catIcon = CATEGORY_ICONS[t.category] || '\uD83D\uDCCB';
582
+
583
+ return h('div', {
584
+ key: nodeId,
585
+ className: 'task-node',
586
+ onClick: function() { setSelectedTask(t); },
587
+ onMouseEnter: function(ev) { setHoveredId(nodeId); setMousePos({ x: ev.clientX, y: ev.clientY }); },
588
+ onMouseMove: function(ev) { if (isHovered) setMousePos({ x: ev.clientX, y: ev.clientY }); },
589
+ onMouseLeave: function() { setHoveredId(null); },
590
+ style: {
591
+ position: 'absolute', left: node.x, top: node.y, width: NODE_W, height: NODE_H,
592
+ background: isHovered ? 'rgba(255,255,255,0.06)' : 'rgba(255,255,255,0.02)',
593
+ border: '1.5px solid ' + (isHovered ? statusColor + '88' : 'rgba(255,255,255,0.12)'),
594
+ borderLeft: '3px solid ' + statusColor,
595
+ borderRadius: 12, padding: '8px 12px', cursor: 'pointer',
596
+ transition: 'all 0.2s', opacity: dim ? 0.2 : 1,
597
+ backdropFilter: 'blur(8px)',
598
+ display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 4, userSelect: 'none',
599
+ },
600
+ },
601
+ // Title row
602
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
603
+ h('span', { style: { fontSize: 12 } }, catIcon),
604
+ h('span', { style: { fontSize: 12, fontWeight: 600, color: '#fff', flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, t.title)
605
+ ),
606
+ // Status + priority + time
607
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 4, flexWrap: 'wrap' } },
608
+ h('span', { style: tagStyle(statusColor) }, t.status.replace('_', ' ')),
609
+ h('span', { style: tagStyle(priColor) }, t.priority),
610
+ h('span', { style: { fontSize: 10, color: 'rgba(255,255,255,0.35)', marginLeft: 'auto' } }, timeAgo(t.createdAt))
611
+ ),
612
+ // Progress bar
613
+ 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 } },
614
+ h('div', { style: { height: '100%', width: t.progress + '%', background: STATUS_COLORS.in_progress, borderRadius: 1 } })
615
+ )
616
+ );
617
+ })
359
618
  )
360
619
  ),
361
620
 
362
- // ─── Stats Bar ─────────────────────────────────────
363
- h(StatsBar, { stats: stats }),
621
+ // Hover tooltip
622
+ hoveredNode && !hoveredNode.isAgent && hoveredNode.task && 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: 200, maxWidth: 300,
627
+ }},
628
+ h('div', { style: { fontSize: 13, fontWeight: 600, color: '#fff', marginBottom: 8 } }, hoveredNode.task.title),
629
+ h('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
630
+ tooltipRow('Status', hoveredNode.task.status.replace('_', ' '), STATUS_COLORS[hoveredNode.task.status]),
631
+ tooltipRow('Priority', hoveredNode.task.priority, PRIORITY_COLORS[hoveredNode.task.priority]),
632
+ tooltipRow('Agent', hoveredNode.task.assignedToName || '-'),
633
+ tooltipRow('Category', hoveredNode.task.category),
634
+ hoveredNode.task.modelUsed && tooltipRow('Model', hoveredNode.task.modelUsed),
635
+ hoveredNode.task.startedAt && tooltipRow('Started', timeAgo(hoveredNode.task.startedAt)),
636
+ hoveredNode.task.actualDurationMs && tooltipRow('Duration', formatDuration(hoveredNode.task.actualDurationMs)),
637
+ hoveredNode.task.progress > 0 && tooltipRow('Progress', hoveredNode.task.progress + '%', STATUS_COLORS.in_progress)
638
+ )
639
+ ),
364
640
 
365
- // ─── Live indicator ────────────────────────────────
366
- h('div', { style: { display: 'flex', alignItems: 'center', gap: 6, marginBottom: 16, fontSize: 12, color: 'var(--text-muted)' } },
367
- h('div', { style: { width: 8, height: 8, borderRadius: '50%', background: '#22c55e', animation: 'pulse 2s infinite' } }),
368
- 'Live updates in real-time',
369
- h('span', { style: { marginLeft: 'auto' } }, filteredTasks.length + ' tasks')
641
+ // Agent hover tooltip
642
+ hoveredNode && hoveredNode.isAgent && h('div', { style: {
643
+ position: 'fixed', left: mousePos.x + 16, top: mousePos.y - 10,
644
+ background: 'rgba(15,17,23,0.95)', backdropFilter: 'blur(12px)',
645
+ border: '1px solid rgba(255,255,255,0.15)', borderRadius: 10,
646
+ padding: '12px 16px', pointerEvents: 'none', zIndex: 1000, minWidth: 180,
647
+ }},
648
+ h('div', { style: { fontSize: 13, fontWeight: 600, color: '#fff', marginBottom: 6 } }, hoveredNode.name),
649
+ tooltipRow('Tasks', String((hoveredNode.children || []).length), ACCENT)
370
650
  ),
371
651
 
372
- // ─── Kanban View ───────────────────────────────────
373
- view === 'kanban' && h('div', {
652
+ // Task detail modal
653
+ selectedTask && h(TaskDetail, { task: selectedTask, onClose: function() { setSelectedTask(null); }, onCancel: cancelTask })
654
+ );
655
+ }
656
+
657
+ // ─── Agent Task Pipeline (for agent-detail workforce tab) ─
658
+ // Reusable mini version scoped to a single agent
659
+ export function AgentTaskPipeline(props) {
660
+ var agentId = props.agentId;
661
+ var _tasks = useState([]);
662
+ var tasks = _tasks[0]; var setTasks = _tasks[1];
663
+ var _loading = useState(true);
664
+ var loading = _loading[0]; var setLoading = _loading[1];
665
+ var _selectedTask = useState(null);
666
+ var selectedTask = _selectedTask[0]; var setSelectedTask = _selectedTask[1];
667
+ var app = useApp();
668
+ var toast = app.toast;
669
+
670
+ useEffect(function() {
671
+ setLoading(true);
672
+ engineCall('/task-pipeline/agent/' + agentId + '?completed=true').then(function(res) {
673
+ setTasks(res?.tasks || []);
674
+ }).catch(function() {}).finally(function() { setLoading(false); });
675
+
676
+ // SSE
677
+ var baseUrl = window.__ENGINE_BASE || '/api/engine';
678
+ var es;
679
+ try {
680
+ es = new EventSource(baseUrl + '/task-pipeline/stream');
681
+ es.onmessage = function(e) {
682
+ try {
683
+ var event = JSON.parse(e.data);
684
+ if (event.task && event.task.assignedTo === agentId) {
685
+ setTasks(function(prev) {
686
+ var idx = prev.findIndex(function(t) { return t.id === event.task.id; });
687
+ if (idx >= 0) { var next = prev.slice(); next[idx] = event.task; return next; }
688
+ return [event.task].concat(prev);
689
+ });
690
+ }
691
+ } catch (err) {}
692
+ };
693
+ } catch (err) {}
694
+ return function() { if (es) es.close(); };
695
+ }, [agentId]);
696
+
697
+ var cancelTask = useCallback(function(taskId) {
698
+ engineCall('/task-pipeline/' + taskId + '/cancel', { method: 'POST' }).then(function() {
699
+ toast('Task cancelled', 'success');
700
+ setSelectedTask(null);
701
+ }).catch(function(err) { toast(err.message, 'error'); });
702
+ }, []);
703
+
704
+ if (loading) return h('div', { style: { padding: 20, textAlign: 'center', color: 'var(--text-muted)', fontSize: 13 } }, 'Loading tasks...');
705
+ if (!tasks.length) return h('div', { style: { padding: 20, textAlign: 'center', color: 'var(--text-muted)', fontSize: 13 } }, 'No pipeline tasks for this agent yet.');
706
+
707
+ var active = tasks.filter(function(t) { return t.status === 'created' || t.status === 'assigned' || t.status === 'in_progress'; });
708
+ var completed = tasks.filter(function(t) { return t.status === 'completed'; });
709
+ var failed = tasks.filter(function(t) { return t.status === 'failed' || t.status === 'cancelled'; });
710
+
711
+ function renderTaskRow(t) {
712
+ var statusColor = STATUS_COLORS[t.status] || '#6b7394';
713
+ var catIcon = CATEGORY_ICONS[t.category] || '\uD83D\uDCCB';
714
+ return h('div', {
715
+ key: t.id,
716
+ onClick: function() { setSelectedTask(t); },
374
717
  style: {
375
- display: 'grid',
376
- gridTemplateColumns: 'repeat(' + COLUMNS.length + ', 1fr)',
377
- gap: 12,
378
- minHeight: 400,
379
- }
718
+ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 12px',
719
+ borderBottom: '1px solid var(--border)', cursor: 'pointer',
720
+ transition: 'background 0.15s',
721
+ },
722
+ onMouseEnter: function(e) { e.currentTarget.style.background = 'var(--bg-secondary)'; },
723
+ onMouseLeave: function(e) { e.currentTarget.style.background = ''; },
380
724
  },
381
- COLUMNS.map(function(col) {
382
- var colTasks = columns[col.key] || [];
383
- return h('div', { key: col.key, style: { background: 'var(--bg-primary)', border: '1px solid var(--border)', borderRadius: 'var(--radius)', overflow: 'hidden' } },
384
- // Column header
385
- h('div', { style: { padding: '10px 12px', borderBottom: '2px solid ' + col.color, display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
386
- h('span', { style: { fontSize: 13, fontWeight: 600 } }, col.label),
387
- h('span', { style: { fontSize: 11, padding: '2px 8px', borderRadius: 10, background: col.color + '22', color: col.color, fontWeight: 600 } }, colTasks.length)
388
- ),
389
- // Cards
390
- h('div', { style: { padding: 8, maxHeight: 500, overflowY: 'auto' } },
391
- colTasks.length === 0
392
- ? h('div', { style: { padding: 20, textAlign: 'center', color: 'var(--text-muted)', fontSize: 12 } }, 'No tasks')
393
- : colTasks.map(function(t) {
394
- return h(TaskCard, { key: t.id, task: t, onSelect: setSelectedTask });
395
- })
396
- )
397
- );
398
- })
399
- ),
725
+ h('span', { style: { fontSize: 14, flexShrink: 0 } }, catIcon),
726
+ h('div', { style: { flex: 1, minWidth: 0 } },
727
+ h('div', { style: { fontSize: 13, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, t.title),
728
+ h('div', { style: { fontSize: 11, color: 'var(--text-muted)', marginTop: 2 } }, t.category + ' \u00B7 ' + timeAgo(t.createdAt))
729
+ ),
730
+ t.status === 'in_progress' && t.progress > 0 && h('div', { style: { width: 40, fontSize: 11, color: STATUS_COLORS.in_progress, fontWeight: 600 } }, t.progress + '%'),
731
+ h('span', { style: { padding: '2px 8px', borderRadius: 10, fontSize: 10, fontWeight: 600, background: statusColor + '22', color: statusColor, flexShrink: 0 } }, t.status.replace('_', ' ')),
732
+ t.actualDurationMs && h('span', { style: { fontSize: 11, color: 'var(--text-muted)', flexShrink: 0 } }, formatDuration(t.actualDurationMs))
733
+ );
734
+ }
400
735
 
401
- // ─── List View ─────────────────────────────────────
402
- view === 'list' && h('div', { className: 'card' },
403
- h('div', { style: { overflowX: 'auto' } },
404
- h('table', { className: 'table' },
405
- h('thead', null,
406
- h('tr', null,
407
- h('th', null, 'Task'),
408
- h('th', null, 'Agent'),
409
- h('th', null, 'Status'),
410
- h('th', null, 'Priority'),
411
- h('th', null, 'Category'),
412
- h('th', null, 'Created'),
413
- h('th', null, 'Duration'),
414
- h('th', null, 'Model')
415
- )
416
- ),
417
- h('tbody', null,
418
- filteredTasks.length === 0
419
- ? h('tr', null, h('td', { colSpan: 8, style: { textAlign: 'center', color: 'var(--text-muted)', padding: 40 } }, 'No tasks found'))
420
- : filteredTasks.map(function(t) {
421
- var statusColor = STATUS_COLORS[t.status] || '#6b7394';
422
- return h('tr', {
423
- key: t.id,
424
- onClick: function() { setSelectedTask(t); },
425
- style: { cursor: 'pointer' }
426
- },
427
- h('td', { style: { maxWidth: 250, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontWeight: 500 } }, t.title),
428
- h('td', { style: { fontSize: 13 } }, t.assignedToName || '-'),
429
- h('td', null,
430
- h('span', { style: { padding: '2px 8px', borderRadius: 10, fontSize: 11, fontWeight: 600, background: statusColor + '22', color: statusColor } },
431
- t.status.replace('_', ' ')
432
- )
433
- ),
434
- h('td', null,
435
- h('span', { style: { fontSize: 11, color: PRIORITY_COLORS[t.priority] || '#6366f1' } }, t.priority)
436
- ),
437
- h('td', { style: { fontSize: 12 } }, (CATEGORY_ICONS[t.category] || '') + ' ' + t.category),
438
- h('td', { style: { fontSize: 12, color: 'var(--text-muted)' } }, timeAgo(t.createdAt)),
439
- h('td', { style: { fontSize: 12 } }, formatDuration(t.actualDurationMs)),
440
- h('td', { style: { fontSize: 12, fontFamily: 'var(--font-mono)' } }, t.modelUsed || t.model || '-')
441
- );
442
- })
443
- )
444
- )
736
+ return h(Fragment, null,
737
+ // Active
738
+ active.length > 0 && h('div', { style: { marginBottom: 16 } },
739
+ h('div', { style: { fontSize: 12, fontWeight: 600, color: STATUS_COLORS.in_progress, marginBottom: 8, display: 'flex', alignItems: 'center', gap: 6 } },
740
+ h('div', { style: { width: 6, height: 6, borderRadius: '50%', background: STATUS_COLORS.in_progress, animation: 'pulse 2s infinite' } }),
741
+ 'Active (' + active.length + ')'
742
+ ),
743
+ h('div', { style: { border: '1px solid var(--border)', borderRadius: 'var(--radius)', overflow: 'hidden' } },
744
+ active.map(renderTaskRow)
445
745
  )
446
746
  ),
447
-
448
- // ─── Task Detail Modal ─────────────────────────────
449
- selectedTask && h(TaskDetail, {
450
- task: selectedTask,
451
- onClose: function() { setSelectedTask(null); },
452
- onCancel: cancelTask
453
- })
747
+ // Completed
748
+ completed.length > 0 && h('div', { style: { marginBottom: 16 } },
749
+ h('div', { style: { fontSize: 12, fontWeight: 600, color: STATUS_COLORS.completed, marginBottom: 8 } }, 'Completed (' + completed.length + ')'),
750
+ h('div', { style: { border: '1px solid var(--border)', borderRadius: 'var(--radius)', overflow: 'hidden' } },
751
+ completed.slice(0, 10).map(renderTaskRow)
752
+ ),
753
+ completed.length > 10 && h('div', { style: { padding: 8, textAlign: 'center', fontSize: 12, color: 'var(--text-muted)' } }, '+ ' + (completed.length - 10) + ' more')
754
+ ),
755
+ // Failed
756
+ failed.length > 0 && h('div', { style: { marginBottom: 16 } },
757
+ h('div', { style: { fontSize: 12, fontWeight: 600, color: STATUS_COLORS.failed, marginBottom: 8 } }, 'Failed (' + failed.length + ')'),
758
+ h('div', { style: { border: '1px solid var(--border)', borderRadius: 'var(--radius)', overflow: 'hidden' } },
759
+ failed.slice(0, 5).map(renderTaskRow)
760
+ )
761
+ ),
762
+ // Detail modal
763
+ selectedTask && h(TaskDetail, { task: selectedTask, onClose: function() { setSelectedTask(null); }, onCancel: cancelTask })
454
764
  );
455
765
  }