flock-core 0.5.0b50__py3-none-any.whl → 0.5.0b51__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 (116) hide show
  1. flock/dashboard/launcher.py +1 -1
  2. flock/frontend/README.md +678 -0
  3. flock/frontend/docs/DESIGN_SYSTEM.md +1980 -0
  4. flock/frontend/index.html +12 -0
  5. flock/frontend/package-lock.json +4347 -0
  6. flock/frontend/package.json +48 -0
  7. flock/frontend/src/App.tsx +79 -0
  8. flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +587 -0
  9. flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +387 -0
  10. flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +640 -0
  11. flock/frontend/src/__tests__/integration/indexeddb-persistence.test.tsx +699 -0
  12. flock/frontend/src/components/common/BuildInfo.tsx +39 -0
  13. flock/frontend/src/components/common/EmptyState.module.css +115 -0
  14. flock/frontend/src/components/common/EmptyState.tsx +128 -0
  15. flock/frontend/src/components/common/ErrorBoundary.module.css +169 -0
  16. flock/frontend/src/components/common/ErrorBoundary.tsx +118 -0
  17. flock/frontend/src/components/common/KeyboardShortcutsDialog.css +251 -0
  18. flock/frontend/src/components/common/KeyboardShortcutsDialog.tsx +151 -0
  19. flock/frontend/src/components/common/LoadingSpinner.module.css +97 -0
  20. flock/frontend/src/components/common/LoadingSpinner.tsx +29 -0
  21. flock/frontend/src/components/controls/PublishControl.css +547 -0
  22. flock/frontend/src/components/controls/PublishControl.test.tsx +543 -0
  23. flock/frontend/src/components/controls/PublishControl.tsx +432 -0
  24. flock/frontend/src/components/details/DetailWindowContainer.tsx +62 -0
  25. flock/frontend/src/components/details/LiveOutputTab.test.tsx +792 -0
  26. flock/frontend/src/components/details/LiveOutputTab.tsx +220 -0
  27. flock/frontend/src/components/details/MessageHistoryTab.tsx +299 -0
  28. flock/frontend/src/components/details/NodeDetailWindow.test.tsx +501 -0
  29. flock/frontend/src/components/details/NodeDetailWindow.tsx +218 -0
  30. flock/frontend/src/components/details/RunStatusTab.tsx +307 -0
  31. flock/frontend/src/components/details/tabs.test.tsx +1015 -0
  32. flock/frontend/src/components/filters/CorrelationIDFilter.module.css +102 -0
  33. flock/frontend/src/components/filters/CorrelationIDFilter.test.tsx +197 -0
  34. flock/frontend/src/components/filters/CorrelationIDFilter.tsx +121 -0
  35. flock/frontend/src/components/filters/FilterBar.module.css +29 -0
  36. flock/frontend/src/components/filters/FilterBar.test.tsx +133 -0
  37. flock/frontend/src/components/filters/FilterBar.tsx +33 -0
  38. flock/frontend/src/components/filters/FilterPills.module.css +79 -0
  39. flock/frontend/src/components/filters/FilterPills.test.tsx +173 -0
  40. flock/frontend/src/components/filters/FilterPills.tsx +67 -0
  41. flock/frontend/src/components/filters/TimeRangeFilter.module.css +91 -0
  42. flock/frontend/src/components/filters/TimeRangeFilter.test.tsx +154 -0
  43. flock/frontend/src/components/filters/TimeRangeFilter.tsx +105 -0
  44. flock/frontend/src/components/graph/AgentNode.test.tsx +75 -0
  45. flock/frontend/src/components/graph/AgentNode.tsx +322 -0
  46. flock/frontend/src/components/graph/GraphCanvas.tsx +406 -0
  47. flock/frontend/src/components/graph/MessageFlowEdge.tsx +128 -0
  48. flock/frontend/src/components/graph/MessageNode.test.tsx +62 -0
  49. flock/frontend/src/components/graph/MessageNode.tsx +116 -0
  50. flock/frontend/src/components/graph/MiniMap.tsx +47 -0
  51. flock/frontend/src/components/graph/TransformEdge.tsx +123 -0
  52. flock/frontend/src/components/layout/DashboardLayout.css +407 -0
  53. flock/frontend/src/components/layout/DashboardLayout.tsx +300 -0
  54. flock/frontend/src/components/layout/Header.module.css +88 -0
  55. flock/frontend/src/components/layout/Header.tsx +52 -0
  56. flock/frontend/src/components/modules/EventLogModule.test.tsx +401 -0
  57. flock/frontend/src/components/modules/EventLogModule.tsx +396 -0
  58. flock/frontend/src/components/modules/EventLogModuleWrapper.tsx +17 -0
  59. flock/frontend/src/components/modules/ModuleRegistry.test.ts +333 -0
  60. flock/frontend/src/components/modules/ModuleRegistry.ts +85 -0
  61. flock/frontend/src/components/modules/ModuleWindow.tsx +155 -0
  62. flock/frontend/src/components/modules/registerModules.ts +20 -0
  63. flock/frontend/src/components/settings/AdvancedSettings.tsx +175 -0
  64. flock/frontend/src/components/settings/AppearanceSettings.tsx +185 -0
  65. flock/frontend/src/components/settings/GraphSettings.tsx +110 -0
  66. flock/frontend/src/components/settings/SettingsPanel.css +327 -0
  67. flock/frontend/src/components/settings/SettingsPanel.tsx +131 -0
  68. flock/frontend/src/components/settings/ThemeSelector.tsx +298 -0
  69. flock/frontend/src/hooks/useKeyboardShortcuts.ts +148 -0
  70. flock/frontend/src/hooks/useModulePersistence.test.ts +442 -0
  71. flock/frontend/src/hooks/useModulePersistence.ts +154 -0
  72. flock/frontend/src/hooks/useModules.ts +139 -0
  73. flock/frontend/src/hooks/usePersistence.ts +139 -0
  74. flock/frontend/src/main.tsx +13 -0
  75. flock/frontend/src/services/api.ts +213 -0
  76. flock/frontend/src/services/indexeddb.test.ts +793 -0
  77. flock/frontend/src/services/indexeddb.ts +794 -0
  78. flock/frontend/src/services/layout.test.ts +437 -0
  79. flock/frontend/src/services/layout.ts +146 -0
  80. flock/frontend/src/services/themeApplicator.ts +140 -0
  81. flock/frontend/src/services/themeService.ts +77 -0
  82. flock/frontend/src/services/websocket.test.ts +595 -0
  83. flock/frontend/src/services/websocket.ts +685 -0
  84. flock/frontend/src/store/filterStore.test.ts +242 -0
  85. flock/frontend/src/store/filterStore.ts +103 -0
  86. flock/frontend/src/store/graphStore.test.ts +186 -0
  87. flock/frontend/src/store/graphStore.ts +414 -0
  88. flock/frontend/src/store/moduleStore.test.ts +253 -0
  89. flock/frontend/src/store/moduleStore.ts +57 -0
  90. flock/frontend/src/store/settingsStore.ts +188 -0
  91. flock/frontend/src/store/streamStore.ts +68 -0
  92. flock/frontend/src/store/uiStore.test.ts +54 -0
  93. flock/frontend/src/store/uiStore.ts +110 -0
  94. flock/frontend/src/store/wsStore.ts +34 -0
  95. flock/frontend/src/styles/index.css +15 -0
  96. flock/frontend/src/styles/scrollbar.css +47 -0
  97. flock/frontend/src/styles/variables.css +488 -0
  98. flock/frontend/src/test/setup.ts +1 -0
  99. flock/frontend/src/types/filters.ts +14 -0
  100. flock/frontend/src/types/graph.ts +55 -0
  101. flock/frontend/src/types/modules.ts +7 -0
  102. flock/frontend/src/types/theme.ts +55 -0
  103. flock/frontend/src/utils/mockData.ts +85 -0
  104. flock/frontend/src/utils/performance.ts +16 -0
  105. flock/frontend/src/utils/transforms.test.ts +860 -0
  106. flock/frontend/src/utils/transforms.ts +323 -0
  107. flock/frontend/src/vite-env.d.ts +17 -0
  108. flock/frontend/tsconfig.json +27 -0
  109. flock/frontend/tsconfig.node.json +11 -0
  110. flock/frontend/vite.config.ts +25 -0
  111. flock/frontend/vitest.config.ts +11 -0
  112. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/METADATA +1 -1
  113. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/RECORD +116 -6
  114. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/WHEEL +0 -0
  115. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/entry_points.txt +0 -0
  116. {flock_core-0.5.0b50.dist-info → flock_core-0.5.0b51.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,220 @@
1
+ import React, { useEffect, useRef, useState, useMemo } from 'react';
2
+ import { useStreamStore, StreamingOutputData } from '../../store/streamStore';
3
+ import { getWebSocketClient } from '../../services/websocket';
4
+
5
+ interface LiveOutputTabProps {
6
+ nodeId: string;
7
+ nodeType: 'agent' | 'message';
8
+ }
9
+
10
+ // Stable empty array to avoid re-renders
11
+ const EMPTY_OUTPUTS: StreamingOutputData[] = [];
12
+
13
+ const LiveOutputTab: React.FC<LiveOutputTabProps> = ({ nodeId, nodeType }) => {
14
+ const [autoScroll, setAutoScroll] = useState(true);
15
+ const [isLoadingHistory, setIsLoadingHistory] = useState(false);
16
+ // Use direct Map access to avoid infinite re-renders
17
+ const outputs = useStreamStore((state) => state.outputs.get(nodeId) ?? EMPTY_OUTPUTS);
18
+ const addOutput = useStreamStore((state) => state.addOutput);
19
+
20
+ // Fetch historical streaming output on mount
21
+ useEffect(() => {
22
+ if (nodeType !== 'agent') return;
23
+
24
+ const fetchHistory = async () => {
25
+ setIsLoadingHistory(true);
26
+ try {
27
+ const response = await fetch(`/api/streaming-history/${nodeId}`);
28
+ if (response.ok) {
29
+ const data = await response.json();
30
+ // Load historical events into the store
31
+ if (data.events && Array.isArray(data.events)) {
32
+ data.events.forEach((event: StreamingOutputData) => {
33
+ addOutput(nodeId, event);
34
+ });
35
+ }
36
+ }
37
+ } catch (error) {
38
+ console.error(`Failed to fetch streaming history for ${nodeId}:`, error);
39
+ } finally {
40
+ setIsLoadingHistory(false);
41
+ }
42
+ };
43
+
44
+ fetchHistory();
45
+ }, [nodeId, nodeType, addOutput]);
46
+
47
+ // Subscribe to WebSocket streaming_output events
48
+ useEffect(() => {
49
+ if (nodeType !== 'agent') return;
50
+
51
+ const wsClient = getWebSocketClient();
52
+
53
+ const handleStreamingOutput = (data: StreamingOutputData) => {
54
+ // Filter by agent name (nodeId is the agent name)
55
+ if (data.agent_name === nodeId) {
56
+ addOutput(nodeId, data);
57
+ }
58
+ };
59
+
60
+ wsClient.on('streaming_output', handleStreamingOutput);
61
+
62
+ return () => {
63
+ wsClient.off('streaming_output', handleStreamingOutput);
64
+ };
65
+ }, [nodeId, nodeType, addOutput]);
66
+
67
+ // Concatenate all llm_tokens into continuous text, logs/stdout/stderr on separate lines
68
+ const displayContent = useMemo(() => {
69
+ let text = '';
70
+ for (const output of outputs) {
71
+ if (output.output_type === 'llm_token') {
72
+ // Append tokens inline without line breaks
73
+ text += output.content;
74
+ } else {
75
+ // Add line break before log/stdout/stderr if previous content doesn't end with one
76
+ if (text && !text.endsWith('\n')) text += '\n';
77
+ text += output.content;
78
+ text += '\n'
79
+ }
80
+ }
81
+ return text;
82
+ }, [outputs]);
83
+
84
+ const containerEndRef = useRef<HTMLDivElement>(null);
85
+
86
+ // Auto-scroll to bottom when new output arrives
87
+ useEffect(() => {
88
+ if (autoScroll && containerEndRef.current) {
89
+ containerEndRef.current.scrollIntoView({ behavior: 'smooth' });
90
+ }
91
+ }, [displayContent, autoScroll]);
92
+
93
+ const hasFinalOutput = outputs.some((o) => o.is_final);
94
+
95
+ return (
96
+ <div
97
+ style={{
98
+ display: 'flex',
99
+ flexDirection: 'column',
100
+ height: '100%',
101
+ background: 'var(--color-bg-elevated)',
102
+ }}
103
+ >
104
+ {/* Auto-scroll toggle */}
105
+ <div
106
+ style={{
107
+ padding: 'var(--space-component-sm) var(--space-component-md)',
108
+ background: 'var(--color-bg-surface)',
109
+ borderBottom: 'var(--border-width-1) solid var(--color-border-subtle)',
110
+ display: 'flex',
111
+ alignItems: 'center',
112
+ justifyContent: 'space-between',
113
+ }}
114
+ >
115
+ <span
116
+ style={{
117
+ color: 'var(--color-text-tertiary)',
118
+ fontSize: 'var(--font-size-caption)',
119
+ fontWeight: 'var(--font-weight-medium)',
120
+ fontFamily: 'var(--font-family-sans)',
121
+ }}
122
+ >
123
+ {outputs.length} {outputs.length === 1 ? 'event' : 'events'}
124
+ </span>
125
+ <label
126
+ style={{
127
+ color: 'var(--color-text-tertiary)',
128
+ fontSize: 'var(--font-size-caption)',
129
+ fontFamily: 'var(--font-family-sans)',
130
+ display: 'flex',
131
+ alignItems: 'center',
132
+ gap: 'var(--gap-sm)',
133
+ cursor: 'pointer',
134
+ userSelect: 'none',
135
+ }}
136
+ >
137
+ <input
138
+ type="checkbox"
139
+ checked={autoScroll}
140
+ onChange={(e) => setAutoScroll(e.target.checked)}
141
+ style={{ cursor: 'pointer', accentColor: 'var(--color-primary-500)' }}
142
+ />
143
+ Auto-scroll
144
+ </label>
145
+ </div>
146
+
147
+ {/* Output display */}
148
+ <div
149
+ data-testid={`live-output-${nodeId}`}
150
+ style={{
151
+ flex: 1,
152
+ overflow: 'auto',
153
+ position: 'relative',
154
+ padding: 'var(--spacing-2)',
155
+ }}
156
+ >
157
+ {isLoadingHistory ? (
158
+ <div
159
+ style={{
160
+ padding: 'var(--space-layout-md)',
161
+ color: 'var(--color-text-muted)',
162
+ fontSize: 'var(--font-size-body-sm)',
163
+ fontFamily: 'var(--font-family-sans)',
164
+ textAlign: 'center',
165
+ }}
166
+ >
167
+ Loading history...
168
+ </div>
169
+ ) : outputs.length === 0 ? (
170
+ <div
171
+ data-testid="empty-output"
172
+ style={{
173
+ padding: 'var(--space-layout-md)',
174
+ color: 'var(--color-text-muted)',
175
+ fontSize: 'var(--font-size-body-sm)',
176
+ fontFamily: 'var(--font-family-sans)',
177
+ textAlign: 'center',
178
+ }}
179
+ >
180
+ Idle - no output
181
+ </div>
182
+ ) : (
183
+ <>
184
+ <pre
185
+ style={{
186
+ margin: 0,
187
+ color: 'var(--color-tertiary-400)',
188
+ fontFamily: 'var(--font-family-mono)',
189
+ fontSize: 'var(--font-size-caption)',
190
+ whiteSpace: 'pre-wrap',
191
+ wordBreak: 'break-word',
192
+ lineHeight: 'var(--line-height-relaxed)',
193
+ }}
194
+ >
195
+ {displayContent}
196
+ </pre>
197
+ {hasFinalOutput && (
198
+ <div
199
+ data-testid="final-marker"
200
+ style={{
201
+ color: 'var(--color-text-muted)',
202
+ fontSize: 'var(--font-size-caption)',
203
+ fontFamily: 'var(--font-family-mono)',
204
+ padding: 'var(--spacing-2) 0',
205
+ borderTop: 'var(--border-width-1) solid var(--color-border-subtle)',
206
+ marginTop: 'var(--spacing-2)',
207
+ }}
208
+ >
209
+ --- End of output ---
210
+ </div>
211
+ )}
212
+ <div ref={containerEndRef} />
213
+ </>
214
+ )}
215
+ </div>
216
+ </div>
217
+ );
218
+ };
219
+
220
+ export default LiveOutputTab;
@@ -0,0 +1,299 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { useGraphStore } from '../../store/graphStore';
3
+
4
+ interface MessageHistoryTabProps {
5
+ nodeId: string;
6
+ nodeType: 'agent' | 'message';
7
+ }
8
+
9
+ interface MessageHistoryEntry {
10
+ id: string;
11
+ type: string;
12
+ direction: 'consumed' | 'published';
13
+ payload: any;
14
+ timestamp: number;
15
+ correlationId: string;
16
+ }
17
+
18
+ const MessageHistoryTab: React.FC<MessageHistoryTabProps> = ({ nodeId, nodeType }) => {
19
+ const messages = useGraphStore((state) => state.messages);
20
+ const agents = useGraphStore((state) => state.agents);
21
+ const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
22
+
23
+ // Build message history based on node type
24
+ const messageHistory = useMemo(() => {
25
+ const history: MessageHistoryEntry[] = [];
26
+
27
+ if (nodeType === 'agent') {
28
+ const agent = agents.get(nodeId);
29
+ if (!agent) return history;
30
+
31
+ // Get all messages
32
+ messages.forEach((message) => {
33
+ // Check if this agent consumed this message
34
+ const isConsumed = agent.subscriptions.includes(message.type);
35
+
36
+ // Check if this agent published this message
37
+ const isPublished = message.producedBy === nodeId;
38
+
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
+ }
49
+
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
+ });
59
+ }
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
+ }
74
+ }
75
+
76
+ // Sort by timestamp (most recent first)
77
+ return history.sort((a, b) => b.timestamp - a.timestamp);
78
+ }, [nodeId, nodeType, messages, agents]);
79
+
80
+ const formatTimestamp = (timestamp: number) => {
81
+ return new Date(timestamp).toLocaleString();
82
+ };
83
+
84
+ const formatPayload = (payload: any) => {
85
+ try {
86
+ return JSON.stringify(payload, null, 2);
87
+ } catch {
88
+ return String(payload);
89
+ }
90
+ };
91
+
92
+ const toggleRowExpansion = (id: string) => {
93
+ setExpandedRows((prev) => {
94
+ const next = new Set(prev);
95
+ if (next.has(id)) {
96
+ next.delete(id);
97
+ } else {
98
+ next.add(id);
99
+ }
100
+ return next;
101
+ });
102
+ };
103
+
104
+ return (
105
+ <div
106
+ data-testid={`message-history-${nodeId}`}
107
+ style={{
108
+ height: '100%',
109
+ overflow: 'auto',
110
+ background: 'var(--color-bg-elevated)',
111
+ color: 'var(--color-text-primary)',
112
+ }}
113
+ >
114
+ {messageHistory.length === 0 ? (
115
+ <div
116
+ data-testid="empty-messages"
117
+ style={{
118
+ padding: 'var(--space-layout-md)',
119
+ color: 'var(--color-text-muted)',
120
+ fontSize: 'var(--font-size-body-sm)',
121
+ fontFamily: 'var(--font-family-sans)',
122
+ textAlign: 'center',
123
+ }}
124
+ >
125
+ No messages yet
126
+ </div>
127
+ ) : (
128
+ <table
129
+ data-testid="message-table"
130
+ style={{
131
+ width: '100%',
132
+ borderCollapse: 'collapse',
133
+ fontSize: 'var(--font-size-caption)',
134
+ fontFamily: 'var(--font-family-sans)',
135
+ }}
136
+ >
137
+ <thead>
138
+ <tr
139
+ style={{
140
+ background: 'var(--color-bg-surface)',
141
+ borderBottom: 'var(--border-width-1) solid var(--color-border-subtle)',
142
+ position: 'sticky',
143
+ top: 0,
144
+ zIndex: 1,
145
+ }}
146
+ >
147
+ <th
148
+ style={{
149
+ padding: 'var(--space-component-sm) var(--space-component-md)',
150
+ textAlign: 'left',
151
+ fontWeight: 'var(--font-weight-semibold)',
152
+ color: 'var(--color-text-secondary)',
153
+ }}
154
+ >
155
+ Time
156
+ </th>
157
+ <th
158
+ style={{
159
+ padding: 'var(--space-component-sm) var(--space-component-md)',
160
+ textAlign: 'left',
161
+ fontWeight: 'var(--font-weight-semibold)',
162
+ color: 'var(--color-text-secondary)',
163
+ }}
164
+ >
165
+ Direction
166
+ </th>
167
+ <th
168
+ style={{
169
+ padding: 'var(--space-component-sm) var(--space-component-md)',
170
+ textAlign: 'left',
171
+ fontWeight: 'var(--font-weight-semibold)',
172
+ color: 'var(--color-text-secondary)',
173
+ }}
174
+ >
175
+ Type
176
+ </th>
177
+ <th
178
+ style={{
179
+ padding: 'var(--space-component-sm) var(--space-component-md)',
180
+ textAlign: 'left',
181
+ fontWeight: 'var(--font-weight-semibold)',
182
+ color: 'var(--color-text-secondary)',
183
+ }}
184
+ >
185
+ Correlation ID
186
+ </th>
187
+ <th
188
+ style={{
189
+ padding: 'var(--space-component-sm) var(--space-component-md)',
190
+ textAlign: 'left',
191
+ fontWeight: 'var(--font-weight-semibold)',
192
+ color: 'var(--color-text-secondary)',
193
+ }}
194
+ >
195
+ Payload
196
+ </th>
197
+ </tr>
198
+ </thead>
199
+ <tbody>
200
+ {messageHistory.map((msg) => {
201
+ const isExpanded = expandedRows.has(msg.id);
202
+ return (
203
+ <React.Fragment key={msg.id}>
204
+ <tr
205
+ data-testid={`message-row-${msg.id}`}
206
+ style={{
207
+ borderBottom: 'var(--border-width-1) solid var(--color-border-subtle)',
208
+ cursor: 'pointer',
209
+ transition: 'var(--transition-colors)',
210
+ }}
211
+ onClick={() => toggleRowExpansion(msg.id)}
212
+ onMouseEnter={(e) => {
213
+ e.currentTarget.style.background = 'var(--color-bg-surface)';
214
+ }}
215
+ onMouseLeave={(e) => {
216
+ e.currentTarget.style.background = 'transparent';
217
+ }}
218
+ >
219
+ <td
220
+ data-testid={`msg-time-${msg.id}`}
221
+ style={{
222
+ padding: 'var(--space-component-sm) var(--space-component-md)',
223
+ whiteSpace: 'nowrap',
224
+ color: 'var(--color-text-tertiary)',
225
+ }}
226
+ >
227
+ {formatTimestamp(msg.timestamp)}
228
+ </td>
229
+ <td
230
+ data-testid={`msg-direction-${msg.id}`}
231
+ style={{
232
+ padding: 'var(--space-component-sm) var(--space-component-md)',
233
+ color:
234
+ msg.direction === 'consumed'
235
+ ? 'var(--color-tertiary-400)'
236
+ : 'var(--color-success-light)',
237
+ fontWeight: 'var(--font-weight-semibold)',
238
+ }}
239
+ >
240
+ {msg.direction === 'consumed' ? '↓ Consumed' : '↑ Published'}
241
+ </td>
242
+ <td
243
+ data-testid={`msg-type-${msg.id}`}
244
+ style={{
245
+ padding: 'var(--space-component-sm) var(--space-component-md)',
246
+ fontFamily: 'var(--font-family-mono)',
247
+ color: 'var(--color-text-primary)',
248
+ }}
249
+ >
250
+ {msg.type}
251
+ </td>
252
+ <td
253
+ data-testid={`msg-correlation-${msg.id}`}
254
+ style={{
255
+ padding: 'var(--space-component-sm) var(--space-component-md)',
256
+ fontFamily: 'var(--font-family-mono)',
257
+ fontSize: 'var(--font-size-overline)',
258
+ color: 'var(--color-text-muted)',
259
+ }}
260
+ >
261
+ {msg.correlationId}
262
+ </td>
263
+ <td
264
+ data-testid={`msg-payload-${msg.id}`}
265
+ style={{
266
+ padding: 'var(--space-component-sm) var(--space-component-md)',
267
+ maxWidth: '300px',
268
+ }}
269
+ >
270
+ <pre
271
+ style={{
272
+ fontSize: 'var(--font-size-overline)',
273
+ fontFamily: 'var(--font-family-mono)',
274
+ maxHeight: isExpanded ? 'none' : '60px',
275
+ overflow: isExpanded ? 'visible' : 'hidden',
276
+ textOverflow: 'ellipsis',
277
+ margin: 0,
278
+ whiteSpace: isExpanded ? 'pre-wrap' : 'nowrap',
279
+ color: 'var(--color-text-secondary)',
280
+ background: 'var(--color-bg-base)',
281
+ padding: 'var(--spacing-1)',
282
+ borderRadius: 'var(--radius-sm)',
283
+ }}
284
+ >
285
+ {formatPayload(msg.payload)}
286
+ </pre>
287
+ </td>
288
+ </tr>
289
+ </React.Fragment>
290
+ );
291
+ })}
292
+ </tbody>
293
+ </table>
294
+ )}
295
+ </div>
296
+ );
297
+ };
298
+
299
+ export default MessageHistoryTab;