flock-core 0.5.0b71__py3-none-any.whl → 0.5.0b75__py3-none-any.whl

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.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (62) hide show
  1. flock/agent.py +39 -1
  2. flock/artifacts.py +17 -10
  3. flock/cli.py +1 -1
  4. flock/dashboard/__init__.py +2 -0
  5. flock/dashboard/collector.py +282 -6
  6. flock/dashboard/events.py +6 -0
  7. flock/dashboard/graph_builder.py +563 -0
  8. flock/dashboard/launcher.py +11 -6
  9. flock/dashboard/models/graph.py +156 -0
  10. flock/dashboard/service.py +175 -14
  11. flock/dashboard/static_v2/assets/index-DFRnI_mt.js +111 -0
  12. flock/dashboard/static_v2/assets/index-fPLNdmp1.css +1 -0
  13. flock/dashboard/static_v2/index.html +13 -0
  14. flock/dashboard/websocket.py +2 -2
  15. flock/engines/dspy_engine.py +27 -8
  16. flock/frontend/README.md +6 -6
  17. flock/frontend/src/App.tsx +23 -31
  18. flock/frontend/src/__tests__/integration/graph-snapshot.test.tsx +647 -0
  19. flock/frontend/src/components/details/DetailWindowContainer.tsx +13 -17
  20. flock/frontend/src/components/details/MessageDetailWindow.tsx +439 -0
  21. flock/frontend/src/components/details/MessageHistoryTab.tsx +128 -53
  22. flock/frontend/src/components/details/RunStatusTab.tsx +79 -38
  23. flock/frontend/src/components/graph/AgentNode.test.tsx +3 -1
  24. flock/frontend/src/components/graph/AgentNode.tsx +8 -6
  25. flock/frontend/src/components/graph/GraphCanvas.tsx +13 -8
  26. flock/frontend/src/components/graph/MessageNode.test.tsx +3 -1
  27. flock/frontend/src/components/graph/MessageNode.tsx +16 -3
  28. flock/frontend/src/components/layout/DashboardLayout.tsx +12 -9
  29. flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +4 -14
  30. flock/frontend/src/components/modules/ModuleRegistry.ts +5 -3
  31. flock/frontend/src/hooks/useModules.ts +12 -4
  32. flock/frontend/src/hooks/usePersistence.ts +5 -3
  33. flock/frontend/src/services/api.ts +3 -19
  34. flock/frontend/src/services/graphService.test.ts +330 -0
  35. flock/frontend/src/services/graphService.ts +75 -0
  36. flock/frontend/src/services/websocket.ts +104 -268
  37. flock/frontend/src/store/filterStore.test.ts +89 -1
  38. flock/frontend/src/store/filterStore.ts +38 -16
  39. flock/frontend/src/store/graphStore.test.ts +538 -173
  40. flock/frontend/src/store/graphStore.ts +374 -465
  41. flock/frontend/src/store/moduleStore.ts +51 -33
  42. flock/frontend/src/store/uiStore.ts +23 -11
  43. flock/frontend/src/types/graph.ts +77 -44
  44. flock/frontend/src/utils/mockData.ts +16 -3
  45. flock/frontend/vite.config.ts +2 -2
  46. flock/orchestrator.py +24 -6
  47. flock/service.py +2 -2
  48. flock/store.py +169 -4
  49. flock/themes/darkmatrix.toml +2 -2
  50. flock/themes/deep.toml +2 -2
  51. flock/themes/neopolitan.toml +4 -4
  52. {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/METADATA +1 -1
  53. {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/RECORD +56 -53
  54. flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +0 -586
  55. flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +0 -391
  56. flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +0 -640
  57. flock/frontend/src/services/websocket.test.ts +0 -595
  58. flock/frontend/src/utils/transforms.test.ts +0 -860
  59. flock/frontend/src/utils/transforms.ts +0 -323
  60. {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/WHEEL +0 -0
  61. {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/entry_points.txt +0 -0
  62. {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,439 @@
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import { Rnd } from 'react-rnd';
3
+ import { useUIStore } from '../../store/uiStore';
4
+ import JsonAttributeRenderer from '../modules/JsonAttributeRenderer';
5
+ import { fetchArtifacts, type ArtifactListItem } from '../../services/api';
6
+
7
+ interface MessageDetailWindowProps {
8
+ nodeId: string;
9
+ }
10
+
11
+ const MessageDetailWindow: React.FC<MessageDetailWindowProps> = ({ nodeId }) => {
12
+ const window = useUIStore((state) => state.detailWindows.get(nodeId));
13
+ const updateDetailWindow = useUIStore((state) => state.updateDetailWindow);
14
+ const closeDetailWindow = useUIStore((state) => state.closeDetailWindow);
15
+
16
+ const [artifact, setArtifact] = useState<ArtifactListItem | null>(null);
17
+ const [isLoading, setIsLoading] = useState(true);
18
+ const [error, setError] = useState<string | null>(null);
19
+
20
+ const handleClose = useCallback(() => {
21
+ closeDetailWindow(nodeId);
22
+ }, [nodeId, closeDetailWindow]);
23
+
24
+ // Fetch artifact details from backend
25
+ // Note: We query recent artifacts and filter client-side by ID
26
+ // because there's no single artifact endpoint yet
27
+ useEffect(() => {
28
+ const fetchArtifactDetails = async () => {
29
+ setIsLoading(true);
30
+ setError(null);
31
+
32
+ try {
33
+ // Fetch recent artifacts with consumption metadata
34
+ const response = await fetchArtifacts({
35
+ limit: 100,
36
+ embedMeta: true,
37
+ });
38
+
39
+ // Find the artifact matching this node ID
40
+ const matchingArtifact = response.items.find((item) => item.id === nodeId);
41
+
42
+ if (!matchingArtifact) {
43
+ throw new Error('Artifact not found');
44
+ }
45
+
46
+ setArtifact(matchingArtifact);
47
+ } catch (err) {
48
+ console.error('Failed to fetch artifact details:', err);
49
+ setError(err instanceof Error ? err.message : 'Unknown error');
50
+ } finally {
51
+ setIsLoading(false);
52
+ }
53
+ };
54
+
55
+ fetchArtifactDetails();
56
+ }, [nodeId]);
57
+
58
+ if (!window) return null;
59
+
60
+ const { position, size } = window;
61
+
62
+ return (
63
+ <Rnd
64
+ position={position}
65
+ size={size}
66
+ onDragStop={(_e, d) => {
67
+ updateDetailWindow(nodeId, {
68
+ position: { x: d.x, y: d.y },
69
+ });
70
+ }}
71
+ onResizeStop={(_e, _direction, ref, _delta, position) => {
72
+ updateDetailWindow(nodeId, {
73
+ size: {
74
+ width: parseInt(ref.style.width, 10),
75
+ height: parseInt(ref.style.height, 10),
76
+ },
77
+ position,
78
+ });
79
+ }}
80
+ minWidth={600}
81
+ minHeight={400}
82
+ bounds="parent"
83
+ dragHandleClassName="window-header"
84
+ style={{
85
+ zIndex: 1000,
86
+ display: 'flex',
87
+ flexDirection: 'column',
88
+ pointerEvents: 'all',
89
+ }}
90
+ >
91
+ <div
92
+ style={{
93
+ display: 'flex',
94
+ flexDirection: 'column',
95
+ width: '100%',
96
+ height: '100%',
97
+ background: 'var(--color-glass-bg)',
98
+ border: 'var(--border-width-1) solid var(--color-glass-border)',
99
+ borderRadius: 'var(--radius-xl)',
100
+ overflow: 'hidden',
101
+ boxShadow: 'var(--shadow-xl)',
102
+ backdropFilter: 'blur(var(--blur-lg))',
103
+ WebkitBackdropFilter: 'blur(var(--blur-lg))',
104
+ }}
105
+ >
106
+ {/* Header */}
107
+ <div
108
+ className="window-header"
109
+ style={{
110
+ display: 'flex',
111
+ alignItems: 'center',
112
+ justifyContent: 'space-between',
113
+ padding: 'var(--space-component-md) var(--space-component-lg)',
114
+ background: 'rgba(42, 42, 50, 0.5)',
115
+ borderBottom: 'var(--border-width-1) solid var(--color-border-subtle)',
116
+ cursor: 'move',
117
+ userSelect: 'none',
118
+ }}
119
+ >
120
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--gap-xs)' }}>
121
+ <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--gap-md)' }}>
122
+ <div
123
+ style={{
124
+ width: '10px',
125
+ height: '10px',
126
+ borderRadius: 'var(--radius-circle)',
127
+ background: 'var(--color-warning)',
128
+ boxShadow: '0 0 8px var(--color-warning)',
129
+ }}
130
+ />
131
+ <span
132
+ style={{
133
+ color: 'var(--color-text-primary)',
134
+ fontSize: 'var(--font-size-body-sm)',
135
+ fontWeight: 'var(--font-weight-semibold)',
136
+ fontFamily: 'var(--font-family-sans)',
137
+ }}
138
+ >
139
+ Message: {artifact?.type || nodeId}
140
+ </span>
141
+ </div>
142
+ <div
143
+ style={{
144
+ fontSize: 'var(--font-size-caption)',
145
+ color: 'var(--color-text-tertiary)',
146
+ fontFamily: 'var(--font-family-mono)',
147
+ paddingLeft: 'calc(10px + var(--gap-md))', // Align with message text
148
+ }}
149
+ >
150
+ id: {nodeId}
151
+ </div>
152
+ </div>
153
+ <button
154
+ onClick={handleClose}
155
+ aria-label="Close window"
156
+ style={{
157
+ background: 'transparent',
158
+ border: 'none',
159
+ color: 'var(--color-text-secondary)',
160
+ fontSize: 'var(--font-size-h3)',
161
+ cursor: 'pointer',
162
+ padding: 'var(--spacing-1) var(--spacing-2)',
163
+ lineHeight: 1,
164
+ borderRadius: 'var(--radius-md)',
165
+ transition: 'var(--transition-colors)',
166
+ display: 'flex',
167
+ alignItems: 'center',
168
+ justifyContent: 'center',
169
+ }}
170
+ onMouseEnter={(e) => {
171
+ e.currentTarget.style.color = 'var(--color-error)';
172
+ e.currentTarget.style.background = 'var(--color-error-bg)';
173
+ }}
174
+ onMouseLeave={(e) => {
175
+ e.currentTarget.style.color = 'var(--color-text-secondary)';
176
+ e.currentTarget.style.background = 'transparent';
177
+ }}
178
+ >
179
+ ×
180
+ </button>
181
+ </div>
182
+
183
+ {/* Content */}
184
+ <div
185
+ style={{
186
+ flex: 1,
187
+ overflow: 'auto',
188
+ background: 'var(--color-bg-elevated)',
189
+ color: 'var(--color-text-primary)',
190
+ padding: 'var(--space-layout-md)',
191
+ }}
192
+ >
193
+ {isLoading ? (
194
+ <div
195
+ style={{
196
+ textAlign: 'center',
197
+ padding: 'var(--space-layout-lg)',
198
+ color: 'var(--color-text-muted)',
199
+ fontSize: 'var(--font-size-body-sm)',
200
+ fontFamily: 'var(--font-family-sans)',
201
+ }}
202
+ >
203
+ Loading artifact details...
204
+ </div>
205
+ ) : error ? (
206
+ <div
207
+ style={{
208
+ textAlign: 'center',
209
+ padding: 'var(--space-layout-lg)',
210
+ color: 'var(--color-error-light)',
211
+ fontSize: 'var(--font-size-body-sm)',
212
+ fontFamily: 'var(--font-family-sans)',
213
+ }}
214
+ >
215
+ Error: {error}
216
+ </div>
217
+ ) : artifact ? (
218
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--gap-lg)' }}>
219
+ {/* Timestamp */}
220
+ <div>
221
+ <div
222
+ style={{
223
+ fontSize: 'var(--font-size-caption)',
224
+ color: 'var(--color-text-tertiary)',
225
+ fontFamily: 'var(--font-family-sans)',
226
+ }}
227
+ >
228
+ {new Date(artifact.created_at).toLocaleString()}
229
+ </div>
230
+ </div>
231
+
232
+ {/* Two-column layout: Metadata (left) + Payload (right) */}
233
+ <div
234
+ style={{
235
+ display: 'grid',
236
+ gridTemplateColumns: 'minmax(250px, 350px) 1fr',
237
+ gap: 'var(--gap-xl)',
238
+ }}
239
+ >
240
+ {/* Metadata Section - Left Column */}
241
+ <div>
242
+ <h3
243
+ style={{
244
+ fontSize: 'var(--font-size-body)',
245
+ fontWeight: 'var(--font-weight-semibold)',
246
+ color: 'var(--color-text-primary)',
247
+ fontFamily: 'var(--font-family-sans)',
248
+ marginBottom: 'var(--space-component-md)',
249
+ }}
250
+ >
251
+ METADATA
252
+ </h3>
253
+ <dl
254
+ style={{
255
+ display: 'grid',
256
+ gridTemplateColumns: 'auto 1fr',
257
+ gap: 'var(--gap-sm) var(--gap-lg)',
258
+ fontSize: 'var(--font-size-body-sm)',
259
+ fontFamily: 'var(--font-family-sans)',
260
+ }}
261
+ >
262
+ <dt style={{ color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-semibold)' }}>
263
+ Produced By
264
+ </dt>
265
+ <dd style={{ color: 'var(--color-text-primary)', margin: 0 }}>{artifact.produced_by}</dd>
266
+
267
+ <dt style={{ color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-semibold)' }}>
268
+ Correlation
269
+ </dt>
270
+ <dd
271
+ style={{
272
+ color: 'var(--color-text-primary)',
273
+ margin: 0,
274
+ fontFamily: 'var(--font-family-mono)',
275
+ fontSize: 'var(--font-size-caption)',
276
+ }}
277
+ >
278
+ {artifact.correlation_id || '—'}
279
+ </dd>
280
+
281
+ <dt style={{ color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-semibold)' }}>
282
+ Partition
283
+ </dt>
284
+ <dd style={{ color: 'var(--color-text-primary)', margin: 0 }}>
285
+ {artifact.partition_key || '—'}
286
+ </dd>
287
+
288
+ <dt style={{ color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-semibold)' }}>
289
+ Tags
290
+ </dt>
291
+ <dd style={{ color: 'var(--color-text-primary)', margin: 0 }}>
292
+ {artifact.tags.length > 0 ? artifact.tags.join(', ') : '—'}
293
+ </dd>
294
+
295
+ <dt style={{ color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-semibold)' }}>
296
+ Visibility
297
+ </dt>
298
+ <dd style={{ color: 'var(--color-text-primary)', margin: 0 }}>
299
+ {artifact.visibility_kind || artifact.visibility?.kind || 'Unknown'}
300
+ </dd>
301
+
302
+ <dt style={{ color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-semibold)' }}>
303
+ Consumed By
304
+ </dt>
305
+ <dd style={{ color: 'var(--color-text-primary)', margin: 0 }}>
306
+ {artifact.consumed_by && artifact.consumed_by.length > 0
307
+ ? artifact.consumed_by.join(', ')
308
+ : '—'}
309
+ </dd>
310
+ </dl>
311
+ </div>
312
+
313
+ {/* Payload Section - Right Column */}
314
+ <div>
315
+ <h3
316
+ style={{
317
+ fontSize: 'var(--font-size-body)',
318
+ fontWeight: 'var(--font-weight-semibold)',
319
+ color: 'var(--color-text-primary)',
320
+ fontFamily: 'var(--font-family-sans)',
321
+ marginBottom: 'var(--space-component-md)',
322
+ }}
323
+ >
324
+ PAYLOAD
325
+ </h3>
326
+ <div
327
+ style={{
328
+ background: 'var(--color-bg-base)',
329
+ border: 'var(--border-default)',
330
+ borderRadius: 'var(--radius-md)',
331
+ padding: 'var(--space-component-md)',
332
+ fontFamily: 'var(--font-family-mono)',
333
+ fontSize: 'var(--font-size-caption)',
334
+ maxHeight: '400px',
335
+ overflow: 'auto',
336
+ }}
337
+ >
338
+ <JsonAttributeRenderer
339
+ value={JSON.stringify(artifact.payload, null, 2)}
340
+ maxStringLength={Number.POSITIVE_INFINITY}
341
+ />
342
+ </div>
343
+ </div>
344
+ </div>
345
+
346
+ {/* Consumption History Section - Full Width Below */}
347
+ <div>
348
+ <h3
349
+ style={{
350
+ fontSize: 'var(--font-size-body)',
351
+ fontWeight: 'var(--font-weight-semibold)',
352
+ color: 'var(--color-text-primary)',
353
+ fontFamily: 'var(--font-family-sans)',
354
+ marginBottom: 'var(--space-component-md)',
355
+ }}
356
+ >
357
+ CONSUMPTION HISTORY
358
+ </h3>
359
+ {artifact.consumptions && artifact.consumptions.length > 0 ? (
360
+ <ul
361
+ style={{
362
+ listStyle: 'none',
363
+ padding: 0,
364
+ margin: 0,
365
+ display: 'flex',
366
+ flexDirection: 'column',
367
+ gap: 'var(--gap-md)',
368
+ }}
369
+ >
370
+ {artifact.consumptions.map((entry) => (
371
+ <li
372
+ key={`${entry.consumer}-${entry.consumed_at}`}
373
+ style={{
374
+ padding: 'var(--space-component-md)',
375
+ background: 'var(--color-bg-surface)',
376
+ border: 'var(--border-default)',
377
+ borderRadius: 'var(--radius-md)',
378
+ fontFamily: 'var(--font-family-sans)',
379
+ fontSize: 'var(--font-size-body-sm)',
380
+ }}
381
+ >
382
+ <div
383
+ style={{
384
+ fontWeight: 'var(--font-weight-semibold)',
385
+ color: 'var(--color-text-primary)',
386
+ marginBottom: 'var(--spacing-1)',
387
+ }}
388
+ >
389
+ {entry.consumer}
390
+ </div>
391
+ <div
392
+ style={{
393
+ fontSize: 'var(--font-size-caption)',
394
+ color: 'var(--color-text-tertiary)',
395
+ }}
396
+ >
397
+ {new Date(entry.consumed_at).toLocaleString()}
398
+ </div>
399
+ {entry.run_id && (
400
+ <div
401
+ style={{
402
+ display: 'inline-block',
403
+ marginTop: 'var(--spacing-2)',
404
+ padding: 'var(--spacing-1) var(--spacing-2)',
405
+ background: 'var(--color-primary-900)',
406
+ color: 'var(--color-primary-300)',
407
+ borderRadius: 'var(--radius-sm)',
408
+ fontSize: 'var(--font-size-overline)',
409
+ fontFamily: 'var(--font-family-mono)',
410
+ }}
411
+ >
412
+ Run {entry.run_id}
413
+ </div>
414
+ )}
415
+ </li>
416
+ ))}
417
+ </ul>
418
+ ) : (
419
+ <p
420
+ style={{
421
+ color: 'var(--color-text-muted)',
422
+ fontSize: 'var(--font-size-body-sm)',
423
+ fontFamily: 'var(--font-family-sans)',
424
+ fontStyle: 'italic',
425
+ }}
426
+ >
427
+ No consumers recorded for this artifact.
428
+ </p>
429
+ )}
430
+ </div>
431
+ </div>
432
+ ) : null}
433
+ </div>
434
+ </div>
435
+ </Rnd>
436
+ );
437
+ };
438
+
439
+ export default MessageDetailWindow;
@@ -1,4 +1,4 @@
1
- import React, { useMemo, useState } from 'react';
1
+ import React, { useEffect, useState, useRef } from 'react';
2
2
  import { useGraphStore } from '../../store/graphStore';
3
3
 
4
4
  interface MessageHistoryTabProps {
@@ -12,70 +12,119 @@ interface MessageHistoryEntry {
12
12
  direction: 'consumed' | 'published';
13
13
  payload: any;
14
14
  timestamp: number;
15
- correlationId: string;
15
+ correlationId: string | null;
16
+ produced_by?: string;
17
+ consumed_at?: string;
16
18
  }
17
19
 
18
- const MessageHistoryTab: React.FC<MessageHistoryTabProps> = ({ nodeId, nodeType }) => {
19
- const messages = useGraphStore((state) => state.messages);
20
- const agents = useGraphStore((state) => state.agents);
20
+ const MessageHistoryTab: React.FC<MessageHistoryTabProps> = ({ nodeId, nodeType: _nodeType }) => {
21
+ const [messageHistory, setMessageHistory] = useState<MessageHistoryEntry[]>([]);
21
22
  const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
23
+ const [isLoading, setIsLoading] = useState(true);
24
+ const [error, setError] = useState<string | null>(null);
25
+ const isInitialLoad = useRef(true);
22
26
 
23
- // Build message history based on node type
24
- const messageHistory = useMemo(() => {
25
- const history: MessageHistoryEntry[] = [];
27
+ // Subscribe to real-time events for this node
28
+ const events = useGraphStore((state) => state.events);
26
29
 
27
- if (nodeType === 'agent') {
28
- const agent = agents.get(nodeId);
29
- if (!agent) return history;
30
+ // Phase 4.1 Feature Gap Fix: Fetch complete message history from backend API
31
+ // Includes both produced AND consumed messages for the node
32
+ useEffect(() => {
33
+ const fetchMessageHistory = async () => {
34
+ // Only show loading spinner on initial load
35
+ if (isInitialLoad.current) {
36
+ setIsLoading(true);
37
+ }
38
+ setError(null);
30
39
 
31
- // Get all messages
32
- messages.forEach((message) => {
33
- // Check if this agent consumed this message
34
- const isConsumed = agent.subscriptions.includes(message.type);
40
+ try {
41
+ const response = await fetch(`/api/artifacts/history/${nodeId}`);
42
+ if (!response.ok) {
43
+ throw new Error(`Failed to fetch message history: ${response.statusText}`);
44
+ }
35
45
 
36
- // Check if this agent published this message
37
- const isPublished = message.producedBy === nodeId;
46
+ const data = await response.json();
38
47
 
39
- if (isConsumed) {
40
- history.push({
41
- id: message.id,
42
- type: message.type,
43
- direction: 'consumed',
44
- payload: message.payload,
45
- timestamp: message.timestamp,
46
- correlationId: message.correlationId,
47
- });
48
- }
48
+ // Convert ISO timestamps to milliseconds
49
+ const history = data.messages.map((msg: any) => ({
50
+ id: msg.id,
51
+ type: msg.type,
52
+ direction: msg.direction,
53
+ payload: msg.payload,
54
+ timestamp: new Date(msg.consumed_at || msg.timestamp).getTime(),
55
+ correlationId: msg.correlation_id,
56
+ produced_by: msg.produced_by,
57
+ consumed_at: msg.consumed_at,
58
+ }));
49
59
 
50
- if (isPublished) {
51
- history.push({
52
- id: `${message.id}-published`,
53
- type: message.type,
54
- direction: 'published',
55
- payload: message.payload,
56
- timestamp: message.timestamp,
57
- correlationId: message.correlationId,
58
- });
60
+ setMessageHistory(history);
61
+ } catch (err) {
62
+ console.error('Failed to fetch message history:', err);
63
+ setError(err instanceof Error ? err.message : 'Unknown error');
64
+ } finally {
65
+ if (isInitialLoad.current) {
66
+ setIsLoading(false);
67
+ isInitialLoad.current = false;
59
68
  }
60
- });
61
- } else if (nodeType === 'message') {
62
- // For message nodes, just show that single message
63
- const message = messages.get(nodeId);
64
- if (message) {
65
- history.push({
66
- id: message.id,
67
- type: message.type,
68
- direction: 'published',
69
- payload: message.payload,
70
- timestamp: message.timestamp,
71
- correlationId: message.correlationId,
72
- });
73
69
  }
70
+ };
71
+
72
+ fetchMessageHistory();
73
+ }, [nodeId]);
74
+
75
+ // Real-time updates: Refetch when new events arrive for this node
76
+ useEffect(() => {
77
+ // Debounce refetch to avoid spamming API
78
+ let refetchTimer: ReturnType<typeof setTimeout> | null = null;
79
+
80
+ const scheduleRefetch = () => {
81
+ if (refetchTimer !== null) {
82
+ clearTimeout(refetchTimer);
83
+ }
84
+
85
+ refetchTimer = setTimeout(async () => {
86
+ refetchTimer = null;
87
+
88
+ try {
89
+ const response = await fetch(`/api/artifacts/history/${nodeId}`);
90
+ if (!response.ok) return;
91
+
92
+ const data = await response.json();
93
+
94
+ const history = data.messages.map((msg: any) => ({
95
+ id: msg.id,
96
+ type: msg.type,
97
+ direction: msg.direction,
98
+ payload: msg.payload,
99
+ timestamp: new Date(msg.consumed_at || msg.timestamp).getTime(),
100
+ correlationId: msg.correlation_id,
101
+ produced_by: msg.produced_by,
102
+ consumed_at: msg.consumed_at,
103
+ }));
104
+
105
+ setMessageHistory(history);
106
+ } catch (err) {
107
+ console.error('Failed to refetch message history:', err);
108
+ }
109
+ }, 500); // 500ms debounce
110
+ };
111
+
112
+ // Check if any recent event relates to this node
113
+ const recentEvents = events.slice(-10); // Check last 10 events
114
+ const hasRelevantEvent = recentEvents.some(
115
+ (event: any) => event.producedBy === nodeId || event.consumedBy === nodeId
116
+ );
117
+
118
+ if (hasRelevantEvent) {
119
+ scheduleRefetch();
74
120
  }
75
121
 
76
- // Sort by timestamp (most recent first)
77
- return history.sort((a, b) => b.timestamp - a.timestamp);
78
- }, [nodeId, nodeType, messages, agents]);
122
+ return () => {
123
+ if (refetchTimer !== null) {
124
+ clearTimeout(refetchTimer);
125
+ }
126
+ };
127
+ }, [events, nodeId]);
79
128
 
80
129
  const formatTimestamp = (timestamp: number) => {
81
130
  return new Date(timestamp).toLocaleString();
@@ -111,7 +160,33 @@ const MessageHistoryTab: React.FC<MessageHistoryTabProps> = ({ nodeId, nodeType
111
160
  color: 'var(--color-text-primary)',
112
161
  }}
113
162
  >
114
- {messageHistory.length === 0 ? (
163
+ {isLoading ? (
164
+ <div
165
+ data-testid="loading-messages"
166
+ style={{
167
+ padding: 'var(--space-layout-md)',
168
+ color: 'var(--color-text-muted)',
169
+ fontSize: 'var(--font-size-body-sm)',
170
+ fontFamily: 'var(--font-family-sans)',
171
+ textAlign: 'center',
172
+ }}
173
+ >
174
+ Loading message history...
175
+ </div>
176
+ ) : error ? (
177
+ <div
178
+ data-testid="error-messages"
179
+ style={{
180
+ padding: 'var(--space-layout-md)',
181
+ color: 'var(--color-error-light)',
182
+ fontSize: 'var(--font-size-body-sm)',
183
+ fontFamily: 'var(--font-family-sans)',
184
+ textAlign: 'center',
185
+ }}
186
+ >
187
+ Error: {error}
188
+ </div>
189
+ ) : messageHistory.length === 0 ? (
115
190
  <div
116
191
  data-testid="empty-messages"
117
192
  style={{