@agenticmail/enterprise 0.5.201 → 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.
@@ -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)
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)
93
179
  );
94
180
  }
95
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,290 +234,512 @@ 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.'),
316
- h('h4', { style: _h4 }, 'How It Works'),
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'),
317
434
  h('ul', { style: _ul },
318
- h('li', null, 'Tasks are automatically recorded when agents are spawned for work'),
319
- h('li', null, 'The pipeline tracks task status from creation through completion'),
320
- h('li', null, 'Real-time updates via SSEno need to refresh'),
321
- h('li', null, 'Smart metadata extraction categorizes tasks automatically')
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')
322
439
  ),
323
440
  h('h4', { style: _h4 }, 'Task Lifecycle'),
324
441
  h('ul', { style: _ul },
325
- h('li', null, h('strong', null, 'Queued'), ' — Task created, waiting to be assigned'),
442
+ h('li', null, h('strong', null, 'Queued'), ' — Waiting to be picked up'),
326
443
  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')
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')
330
447
  ),
331
- h('div', { style: _tip }, 'Tip: Click any task card to see full details including model used, tokens consumed, duration, and results.')
448
+ h('div', { style: _tip }, 'Tip: Sub-tasks appear as children of their parent task. The arrow system shows task delegation flow.')
332
449
  )
333
450
  ),
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);
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
+ });
344
516
  })
345
517
  ),
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')
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
+ })
359
598
  )
360
599
  ),
361
600
 
362
- // ─── Stats Bar ─────────────────────────────────────
363
- h(StatsBar, { stats: stats }),
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
+ ),
364
620
 
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')
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)
370
630
  ),
371
631
 
372
- // ─── Kanban View ───────────────────────────────────
373
- view === 'kanban' && h('div', {
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); },
374
697
  style: {
375
- display: 'grid',
376
- gridTemplateColumns: 'repeat(' + COLUMNS.length + ', 1fr)',
377
- gap: 12,
378
- minHeight: 400,
379
- }
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 = ''; },
380
704
  },
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
- ),
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
+ }
400
715
 
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
- )
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)
445
725
  )
446
726
  ),
447
-
448
- // ─── Task Detail Modal ─────────────────────────────
449
- selectedTask && h(TaskDetail, {
450
- task: selectedTask,
451
- onClose: function() { setSelectedTask(null); },
452
- onCancel: cancelTask
453
- })
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 })
454
744
  );
455
745
  }