@synergenius/flow-weaver-pack-weaver 0.9.53 → 0.9.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Convert a progressive stream of execution events into TimelineEntry[]
3
+ * that ExecutionTimeline can render.
4
+ *
5
+ * Handles: node-start/complete/error, bot-started/completed/failed,
6
+ * audit:plan-created, cost-update.
7
+ */
8
+
9
+ const React = require('react');
10
+ const { useMemo, useState, useEffect, useRef } = React;
11
+
12
+ // Types inlined — these are provided by @fw/plugin-ui-kit at runtime
13
+ interface StreamEvent {
14
+ id: number;
15
+ type: string;
16
+ timestamp: number;
17
+ taskId?: string;
18
+ data?: Record<string, unknown>;
19
+ }
20
+
21
+ interface TimelineEntry {
22
+ id: string;
23
+ timestamp: Date;
24
+ type: 'task-started' | 'node-started' | 'node-completed' | 'node-failed' | 'agent-request' | 'user-action' | 'task-completed' | 'task-failed';
25
+ nodeId?: string;
26
+ label: string;
27
+ detail?: string;
28
+ outputs?: Array<{ portLabel: string; value: unknown }>;
29
+ duration?: number;
30
+ color?: string;
31
+ icon?: string;
32
+ }
33
+
34
+ export interface StreamTimelineState {
35
+ timeline: TimelineEntry[];
36
+ phase: 'idle' | 'planning' | 'executing' | 'completed' | 'failed';
37
+ instruction: string | null;
38
+ elapsed: number;
39
+ cost: number | null;
40
+ plan: {
41
+ summary: string;
42
+ steps: Array<{ id: string; operation: string; description: string }>;
43
+ } | null;
44
+ awaitingApproval: boolean;
45
+ }
46
+
47
+ export function useStreamTimeline(events: StreamEvent[], isDone: boolean): StreamTimelineState {
48
+ const [elapsed, setElapsed] = useState(0);
49
+ const startTimeRef = useRef(null as number | null);
50
+
51
+ // Track start time from first event; reset when events are cleared (new run)
52
+ useEffect(() => {
53
+ if (events.length === 0) {
54
+ startTimeRef.current = null;
55
+ setElapsed(0);
56
+ } else if (startTimeRef.current === null) {
57
+ startTimeRef.current = events[0]!.timestamp;
58
+ }
59
+ }, [events.length]);
60
+
61
+ // Elapsed timer — ticks every second while streaming
62
+ useEffect(() => {
63
+ if (isDone || !startTimeRef.current) return;
64
+ const tick = () => setElapsed(Date.now() - (startTimeRef.current ?? Date.now()));
65
+ tick();
66
+ const interval = setInterval(tick, 1000);
67
+ return () => clearInterval(interval);
68
+ }, [isDone, events.length]); // re-run when events arrive to start timer
69
+
70
+ // Convert events to timeline entries.
71
+ // Collapses node-start + node-complete into a single entry per node:
72
+ // - While a node is running: shows as 'node-started' (spinner)
73
+ // - When it completes/fails: replaces with 'node-completed'/'node-failed' (with duration)
74
+ const timeline = useMemo(() => {
75
+ const entries: TimelineEntry[] = [];
76
+ // Map nodeId → index in entries[] so we can replace start with complete
77
+ const nodeEntryIndex = new Map<string, number>();
78
+ const nodeStarts = new Map<string, number>();
79
+ let idCounter = 0;
80
+
81
+ for (const event of events) {
82
+ const d = event.data ?? {};
83
+
84
+ switch (event.type) {
85
+ case 'bot-started':
86
+ entries.push({
87
+ id: `s-${idCounter++}`,
88
+ timestamp: new Date(event.timestamp),
89
+ type: 'task-started',
90
+ label: 'Task started',
91
+ detail: d.instruction as string | undefined,
92
+ });
93
+ break;
94
+
95
+ case 'node-start': {
96
+ const nodeId = d.nodeId as string;
97
+ if (nodeId) nodeStarts.set(nodeId, event.timestamp);
98
+ const idx = entries.length;
99
+ nodeEntryIndex.set(nodeId, idx);
100
+ entries.push({
101
+ id: `s-${idCounter++}`,
102
+ timestamp: new Date(event.timestamp),
103
+ type: 'node-started',
104
+ nodeId,
105
+ label: (d.label as string) ?? (d.nodeType as string) ?? nodeId ?? 'Node',
106
+ });
107
+ break;
108
+ }
109
+
110
+ case 'node-complete': {
111
+ const nodeId = d.nodeId as string;
112
+ const startTs = nodeStarts.get(nodeId);
113
+ const duration =
114
+ (d.durationMs as number) ?? (startTs ? event.timestamp - startTs : undefined);
115
+ if (nodeId) nodeStarts.delete(nodeId);
116
+
117
+ const completed: TimelineEntry = {
118
+ id: `s-${idCounter++}`,
119
+ timestamp: new Date(startTs ?? event.timestamp),
120
+ type: 'node-completed',
121
+ nodeId,
122
+ label: (d.label as string) ?? (d.nodeType as string) ?? nodeId ?? 'Node',
123
+ duration,
124
+ };
125
+
126
+ // Replace the node-started entry in-place
127
+ const existingIdx = nodeEntryIndex.get(nodeId);
128
+ if (existingIdx != null && entries[existingIdx]) {
129
+ entries[existingIdx] = completed;
130
+ nodeEntryIndex.delete(nodeId);
131
+ } else {
132
+ entries.push(completed);
133
+ }
134
+ break;
135
+ }
136
+
137
+ case 'node-error': {
138
+ const nodeId = d.nodeId as string;
139
+ const startTs = nodeStarts.get(nodeId);
140
+ const duration =
141
+ (d.durationMs as number) ?? (startTs ? event.timestamp - startTs : undefined);
142
+ if (nodeId) nodeStarts.delete(nodeId);
143
+
144
+ const failed: TimelineEntry = {
145
+ id: `s-${idCounter++}`,
146
+ timestamp: new Date(startTs ?? event.timestamp),
147
+ type: 'node-failed',
148
+ nodeId,
149
+ label: (d.label as string) ?? (d.nodeType as string) ?? nodeId ?? 'Node',
150
+ detail: d.error as string | undefined,
151
+ duration,
152
+ };
153
+
154
+ // Replace the node-started entry in-place
155
+ const existingIdx = nodeEntryIndex.get(nodeId);
156
+ if (existingIdx != null && entries[existingIdx]) {
157
+ entries[existingIdx] = failed;
158
+ nodeEntryIndex.delete(nodeId);
159
+ } else {
160
+ entries.push(failed);
161
+ }
162
+ break;
163
+ }
164
+
165
+ case 'bot-completed':
166
+ entries.push({
167
+ id: `s-${idCounter++}`,
168
+ timestamp: new Date(event.timestamp),
169
+ type: 'task-completed',
170
+ label: (d.success as boolean) ? 'Task completed' : 'Task finished',
171
+ detail: d.summary as string | undefined,
172
+ });
173
+ break;
174
+
175
+ case 'bot-failed':
176
+ entries.push({
177
+ id: `s-${idCounter++}`,
178
+ timestamp: new Date(event.timestamp),
179
+ type: 'task-failed',
180
+ label: 'Task failed',
181
+ detail: d.error as string | undefined,
182
+ });
183
+ break;
184
+
185
+ // Skip audit, cost-update, done — they're used for metadata, not timeline
186
+ default:
187
+ break;
188
+ }
189
+ }
190
+
191
+ return entries;
192
+ }, [events]);
193
+
194
+ // Extract metadata from events
195
+ const { phase, instruction, cost, plan, awaitingApproval } = useMemo(() => {
196
+ let phase: StreamTimelineState['phase'] = 'idle';
197
+ let instruction: string | null = null;
198
+ let cost: number | null = null;
199
+ let plan: StreamTimelineState['plan'] = null;
200
+ let awaitingApproval = false;
201
+
202
+ for (const event of events) {
203
+ const d = event.data ?? {};
204
+
205
+ if (event.type === 'bot-started') {
206
+ phase = 'planning';
207
+ instruction = (d.instruction as string) ?? null;
208
+ } else if (event.type === 'node-start' && phase !== 'completed' && phase !== 'failed') {
209
+ phase = 'executing';
210
+ } else if (event.type === 'bot-completed') {
211
+ phase = (d.success as boolean) ? 'completed' : 'failed';
212
+ } else if (event.type === 'bot-failed') {
213
+ phase = 'failed';
214
+ } else if (event.type === 'cost-update') {
215
+ cost = (d.totalCost as number) ?? cost;
216
+ } else if (event.type === 'audit:plan-created' || event.type === 'plan-created') {
217
+ // Plan data may be at d.plan (structured) or directly on d (audit event)
218
+ const planData = (d.plan as Record<string, unknown>) ?? d;
219
+ const summary = (planData.summary as string) ?? '';
220
+ const steps =
221
+ (planData.steps as Array<{ id: string; operation: string; description: string }>) ?? [];
222
+ if (summary || steps.length > 0) {
223
+ plan = { summary, steps };
224
+ }
225
+ } else if (event.type === 'approval-needed') {
226
+ awaitingApproval = true;
227
+ } else if (event.type === 'audit:approval-decision') {
228
+ awaitingApproval = false;
229
+ }
230
+ }
231
+
232
+ if (isDone && phase !== 'completed' && phase !== 'failed') {
233
+ phase = 'completed'; // Stream ended but no explicit completion event
234
+ }
235
+
236
+ return { phase, instruction, cost, plan, awaitingApproval };
237
+ }, [events, isDone]);
238
+
239
+ return { timeline, phase, instruction, elapsed, cost, plan, awaitingApproval };
240
+ }