flock-core 0.5.0b65__py3-none-any.whl → 0.5.0b71__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 (56) hide show
  1. flock/cli.py +74 -2
  2. flock/engines/dspy_engine.py +41 -5
  3. flock/examples.py +4 -1
  4. flock/frontend/README.md +15 -1
  5. flock/frontend/package-lock.json +2 -2
  6. flock/frontend/package.json +1 -1
  7. flock/frontend/src/App.tsx +74 -6
  8. flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +4 -5
  9. flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +7 -3
  10. flock/frontend/src/components/filters/ArtifactTypeFilter.tsx +21 -0
  11. flock/frontend/src/components/filters/FilterFlyout.module.css +104 -0
  12. flock/frontend/src/components/filters/FilterFlyout.tsx +80 -0
  13. flock/frontend/src/components/filters/FilterPills.module.css +186 -45
  14. flock/frontend/src/components/filters/FilterPills.test.tsx +115 -99
  15. flock/frontend/src/components/filters/FilterPills.tsx +120 -44
  16. flock/frontend/src/components/filters/ProducerFilter.tsx +21 -0
  17. flock/frontend/src/components/filters/SavedFiltersControl.module.css +60 -0
  18. flock/frontend/src/components/filters/SavedFiltersControl.test.tsx +158 -0
  19. flock/frontend/src/components/filters/SavedFiltersControl.tsx +159 -0
  20. flock/frontend/src/components/filters/TagFilter.tsx +21 -0
  21. flock/frontend/src/components/filters/TimeRangeFilter.module.css +24 -0
  22. flock/frontend/src/components/filters/TimeRangeFilter.tsx +6 -1
  23. flock/frontend/src/components/filters/VisibilityFilter.tsx +21 -0
  24. flock/frontend/src/components/graph/GraphCanvas.tsx +24 -0
  25. flock/frontend/src/components/layout/DashboardLayout.css +13 -0
  26. flock/frontend/src/components/layout/DashboardLayout.tsx +8 -24
  27. flock/frontend/src/components/modules/HistoricalArtifactsModule.module.css +288 -0
  28. flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +460 -0
  29. flock/frontend/src/components/modules/HistoricalArtifactsModuleWrapper.tsx +13 -0
  30. flock/frontend/src/components/modules/ModuleRegistry.ts +7 -1
  31. flock/frontend/src/components/modules/registerModules.ts +9 -10
  32. flock/frontend/src/hooks/useModules.ts +11 -1
  33. flock/frontend/src/services/api.ts +140 -0
  34. flock/frontend/src/services/indexeddb.ts +56 -2
  35. flock/frontend/src/services/websocket.ts +129 -0
  36. flock/frontend/src/store/filterStore.test.ts +105 -185
  37. flock/frontend/src/store/filterStore.ts +173 -26
  38. flock/frontend/src/store/graphStore.test.ts +19 -0
  39. flock/frontend/src/store/graphStore.ts +166 -27
  40. flock/frontend/src/types/filters.ts +34 -1
  41. flock/frontend/src/types/graph.ts +7 -0
  42. flock/frontend/src/utils/artifacts.ts +24 -0
  43. flock/orchestrator.py +23 -1
  44. flock/service.py +146 -9
  45. flock/store.py +971 -24
  46. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b71.dist-info}/METADATA +26 -1
  47. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b71.dist-info}/RECORD +50 -43
  48. flock/frontend/src/components/filters/FilterBar.module.css +0 -29
  49. flock/frontend/src/components/filters/FilterBar.test.tsx +0 -133
  50. flock/frontend/src/components/filters/FilterBar.tsx +0 -33
  51. flock/frontend/src/components/modules/EventLogModule.test.tsx +0 -401
  52. flock/frontend/src/components/modules/EventLogModule.tsx +0 -396
  53. flock/frontend/src/components/modules/EventLogModuleWrapper.tsx +0 -17
  54. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b71.dist-info}/WHEEL +0 -0
  55. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b71.dist-info}/entry_points.txt +0 -0
  56. {flock_core-0.5.0b65.dist-info → flock_core-0.5.0b71.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,460 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { fetchArtifactSummary, fetchArtifacts, type ArtifactListItem, type ArtifactQueryOptions } from '../../services/api';
3
+ import { mapArtifactToMessage } from '../../utils/artifacts';
4
+ import { useFilterStore } from '../../store/filterStore';
5
+ import { useGraphStore } from '../../store/graphStore';
6
+ import { useUIStore } from '../../store/uiStore';
7
+ import type { ModuleContext } from './ModuleRegistry';
8
+ import JsonAttributeRenderer from './JsonAttributeRenderer';
9
+ import styles from './HistoricalArtifactsModule.module.css';
10
+
11
+ const PAGE_SIZE = 100;
12
+ const ITEM_HEIGHT = 48;
13
+ const MIN_VISIBLE_ITEMS = 8;
14
+
15
+ type TimeRangeSelection = ReturnType<typeof useFilterStore.getState>['timeRange'];
16
+
17
+ type HistoricalArtifactsModuleProps = {
18
+ context: ModuleContext;
19
+ };
20
+
21
+ const resolveTimeRangeToIso = (range: TimeRangeSelection): { from?: string; to?: string } => {
22
+ const now = Date.now();
23
+ if (range.preset === 'last5min') {
24
+ return {
25
+ from: new Date(now - 5 * 60 * 1000).toISOString(),
26
+ to: new Date(now).toISOString(),
27
+ };
28
+ }
29
+ if (range.preset === 'last10min') {
30
+ return {
31
+ from: new Date(now - 10 * 60 * 1000).toISOString(),
32
+ to: new Date(now).toISOString(),
33
+ };
34
+ }
35
+ if (range.preset === 'last1hour') {
36
+ return {
37
+ from: new Date(now - 60 * 60 * 1000).toISOString(),
38
+ to: new Date(now).toISOString(),
39
+ };
40
+ }
41
+ if (range.preset === 'custom' && range.start && range.end) {
42
+ return {
43
+ from: new Date(range.start).toISOString(),
44
+ to: new Date(range.end).toISOString(),
45
+ };
46
+ }
47
+ return {};
48
+ };
49
+
50
+ const HistoricalArtifactsModule: React.FC<HistoricalArtifactsModuleProps> = ({ context }) => {
51
+ // Context reserved for future extensions (module lifecycle expects prop)
52
+ void context;
53
+ const correlationId = useFilterStore((state) => state.correlationId);
54
+ const timeRange = useFilterStore((state) => state.timeRange);
55
+ const selectedArtifactTypes = useFilterStore((state) => state.selectedArtifactTypes);
56
+ const selectedProducers = useFilterStore((state) => state.selectedProducers);
57
+ const selectedTags = useFilterStore((state) => state.selectedTags);
58
+ const selectedVisibility = useFilterStore((state) => state.selectedVisibility);
59
+ const setSummary = useFilterStore((state) => state.setSummary);
60
+ const updateAvailableCorrelationIds = useFilterStore((state) => state.updateAvailableCorrelationIds);
61
+ const summary = useFilterStore((state) => state.summary);
62
+
63
+ const [artifacts, setArtifacts] = useState<ArtifactListItem[]>([]);
64
+ const [nextOffset, setNextOffset] = useState(0);
65
+ const [total, setTotal] = useState(0);
66
+ const [loading, setLoading] = useState(false);
67
+ const [error, setError] = useState<string | null>(null);
68
+ const [selectedArtifactId, setSelectedArtifactId] = useState<string | null>(null);
69
+
70
+ const lastAutoLoadIndex = useRef(-1);
71
+ const virtualScrollRef = useRef<HTMLDivElement | null>(null);
72
+ const [virtualRange, setVirtualRange] = useState({ start: 0, end: MIN_VISIBLE_ITEMS });
73
+
74
+ const selectedArtifact = useMemo(
75
+ () => artifacts.find((artifact) => artifact.id === selectedArtifactId) ?? null,
76
+ [artifacts, selectedArtifactId]
77
+ );
78
+
79
+ const hasMore = total > nextOffset;
80
+
81
+ useEffect(() => {
82
+ if (selectedArtifactId && !artifacts.some((artifact) => artifact.id === selectedArtifactId)) {
83
+ setSelectedArtifactId(null);
84
+ }
85
+ }, [artifacts, selectedArtifactId]);
86
+
87
+ const buildQueryOptions = useCallback(
88
+ (offset: number): ArtifactQueryOptions => {
89
+ const range = resolveTimeRangeToIso(timeRange);
90
+ return {
91
+ types: selectedArtifactTypes,
92
+ producers: selectedProducers,
93
+ tags: selectedTags,
94
+ visibility: selectedVisibility,
95
+ correlationId,
96
+ from: range.from,
97
+ to: range.to,
98
+ limit: PAGE_SIZE,
99
+ offset,
100
+ embedMeta: true,
101
+ };
102
+ },
103
+ [timeRange, selectedArtifactTypes, selectedProducers, selectedTags, selectedVisibility, correlationId]
104
+ );
105
+
106
+ const mergeCorrelationMetadata = useCallback(
107
+ (items: ArtifactListItem[]) => {
108
+ if (items.length === 0) return;
109
+
110
+ const existing = useFilterStore.getState().availableCorrelationIds;
111
+ const merged = new Map(existing.map((item) => [item.correlation_id, { ...item }]));
112
+
113
+ items.forEach((item) => {
114
+ if (!item.correlation_id) return;
115
+ const timestamp = new Date(item.created_at).getTime();
116
+ const current = merged.get(item.correlation_id);
117
+ if (current) {
118
+ current.artifact_count += 1;
119
+ current.first_seen = Math.min(current.first_seen, timestamp);
120
+ } else {
121
+ merged.set(item.correlation_id, {
122
+ correlation_id: item.correlation_id,
123
+ first_seen: timestamp,
124
+ artifact_count: 1,
125
+ run_count: 0,
126
+ });
127
+ }
128
+ });
129
+
130
+ updateAvailableCorrelationIds(Array.from(merged.values()));
131
+ },
132
+ [updateAvailableCorrelationIds]
133
+ );
134
+
135
+ const loadArtifacts = useCallback(
136
+ async (reset: boolean) => {
137
+ setLoading(true);
138
+ try {
139
+ const offset = reset ? 0 : nextOffset;
140
+ const queryOptions = buildQueryOptions(offset);
141
+ const response = await fetchArtifacts(queryOptions);
142
+
143
+ setArtifacts((prev) => (reset ? response.items : [...prev, ...response.items]));
144
+ setNextOffset(offset + response.pagination.limit);
145
+ setTotal(response.pagination.total);
146
+ setError(null);
147
+
148
+ mergeCorrelationMetadata(response.items);
149
+
150
+ if (response.items.length > 0) {
151
+ const graphStore = useGraphStore.getState();
152
+ graphStore.batchUpdate({ messages: response.items.map(mapArtifactToMessage) });
153
+ const uiState = useUIStore.getState();
154
+ if (uiState.mode === 'agent') {
155
+ graphStore.generateAgentViewGraph();
156
+ } else {
157
+ graphStore.generateBlackboardViewGraph();
158
+ }
159
+ graphStore.applyFilters();
160
+ }
161
+
162
+ const summaryResponse = await fetchArtifactSummary({
163
+ ...queryOptions,
164
+ limit: undefined,
165
+ offset: undefined,
166
+ });
167
+ setSummary(summaryResponse);
168
+ } catch (err) {
169
+ console.error('[HistoricalArtifactsModule] Failed to load artifacts', err);
170
+ setError('Failed to load artifacts');
171
+ } finally {
172
+ setLoading(false);
173
+ }
174
+ },
175
+ [buildQueryOptions, mergeCorrelationMetadata, nextOffset, setSummary]
176
+ );
177
+
178
+ useEffect(() => {
179
+ lastAutoLoadIndex.current = -1;
180
+ loadArtifacts(true);
181
+ }, [loadArtifacts]);
182
+
183
+ const rows = useMemo(
184
+ () =>
185
+ artifacts.map((artifact) => ({
186
+ id: artifact.id,
187
+ timestamp: new Date(artifact.created_at).toLocaleString(),
188
+ type: artifact.type,
189
+ producedBy: artifact.produced_by,
190
+ correlationId: artifact.correlation_id ?? '—',
191
+ tags: artifact.tags.join(', ') || '—',
192
+ visibility: artifact.visibility_kind || artifact.visibility?.kind || 'Unknown',
193
+ consumedCount: artifact.consumptions?.length ?? 0,
194
+ })),
195
+ [artifacts]
196
+ );
197
+
198
+ const handleLoadMore = () => {
199
+ if (!loading && hasMore) {
200
+ lastAutoLoadIndex.current = -1;
201
+ loadArtifacts(false);
202
+ }
203
+ };
204
+ const listHeight = useMemo(() => Math.max(MIN_VISIBLE_ITEMS, Math.min(rows.length || MIN_VISIBLE_ITEMS, 12)) * ITEM_HEIGHT, [rows.length]);
205
+
206
+ useEffect(() => {
207
+ const container = virtualScrollRef.current;
208
+ if (!container) {
209
+ return;
210
+ }
211
+
212
+ const handleScroll = () => {
213
+ const totalItems = rows.length;
214
+ if (totalItems === 0) {
215
+ setVirtualRange({ start: 0, end: MIN_VISIBLE_ITEMS });
216
+ return;
217
+ }
218
+
219
+ const scrollTop = container.scrollTop;
220
+ const viewportHeight = container.clientHeight;
221
+ const startIndex = Math.max(0, Math.floor(scrollTop / ITEM_HEIGHT) - 5);
222
+ const endIndex = Math.min(totalItems, Math.ceil((scrollTop + viewportHeight) / ITEM_HEIGHT) + 5);
223
+ setVirtualRange({ start: startIndex, end: endIndex });
224
+
225
+ if (hasMore && !loading && endIndex >= totalItems - 5 && lastAutoLoadIndex.current !== endIndex) {
226
+ lastAutoLoadIndex.current = endIndex;
227
+ handleLoadMore();
228
+ }
229
+ };
230
+
231
+ handleScroll();
232
+ container.addEventListener('scroll', handleScroll);
233
+ return () => container.removeEventListener('scroll', handleScroll);
234
+ }, [handleLoadMore, hasMore, loading, rows.length]);
235
+
236
+ useEffect(() => {
237
+ const container = virtualScrollRef.current;
238
+ if (container) {
239
+ container.scrollTop = 0;
240
+ }
241
+ setVirtualRange({ start: 0, end: Math.max(MIN_VISIBLE_ITEMS, Math.min(rows.length, MIN_VISIBLE_ITEMS * 2)) });
242
+ lastAutoLoadIndex.current = -1;
243
+ }, [rows.length]);
244
+
245
+ const retentionInfo = useMemo(() => {
246
+ if (!summary?.earliest_created_at || !summary?.latest_created_at) {
247
+ return null;
248
+ }
249
+ const earliest = new Date(summary.earliest_created_at);
250
+ const latest = new Date(summary.latest_created_at);
251
+ const spanMs = Math.max(0, latest.getTime() - earliest.getTime());
252
+ const spanDays = spanMs / (1000 * 60 * 60 * 24);
253
+ let spanLabel: string;
254
+ if (spanDays >= 2) {
255
+ spanLabel = `${spanDays.toFixed(1)} days`;
256
+ } else if (spanDays >= 0.5) {
257
+ spanLabel = `${(spanDays * 24).toFixed(0)} hours`;
258
+ } else {
259
+ spanLabel = `${Math.max(1, Math.round(spanMs / (1000 * 60)))} minutes`;
260
+ }
261
+ return {
262
+ earliest: earliest.toLocaleString(),
263
+ latest: latest.toLocaleString(),
264
+ spanLabel,
265
+ };
266
+ }, [summary]);
267
+
268
+ return (
269
+ <div className={styles.container}>
270
+ <header className={styles.header}>
271
+ <div className={styles.metrics}>
272
+ <div>
273
+ <span className={styles.metricLabel}>Artifacts</span>
274
+ <span className={styles.metricValue}>{total}</span>
275
+ </div>
276
+ <div>
277
+ <span className={styles.metricLabel}>Earliest</span>
278
+ <span className={styles.metricValue}>
279
+ {summary?.earliest_created_at ? new Date(summary.earliest_created_at).toLocaleString() : '—'}
280
+ </span>
281
+ </div>
282
+ <div>
283
+ <span className={styles.metricLabel}>Latest</span>
284
+ <span className={styles.metricValue}>
285
+ {summary?.latest_created_at ? new Date(summary.latest_created_at).toLocaleString() : '—'}
286
+ </span>
287
+ </div>
288
+ </div>
289
+ <div className={styles.actions}>
290
+ <button type="button" onClick={() => loadArtifacts(true)} disabled={loading}>
291
+ Refresh
292
+ </button>
293
+ <button type="button" onClick={handleLoadMore} disabled={loading || !hasMore}>
294
+ Load Older
295
+ </button>
296
+ </div>
297
+ </header>
298
+
299
+ {retentionInfo && (
300
+ <div className={styles.retentionBanner}>
301
+ <span>
302
+ Historical window: <strong>{retentionInfo.spanLabel}</strong> (oldest artifact {retentionInfo.earliest})
303
+ </span>
304
+ <span>
305
+ Latest artifact recorded at <strong>{retentionInfo.latest}</strong>.{' '}
306
+ {hasMore ? 'Use “Load Older” to fetch additional retained history.' : 'You are viewing the full retained history.'}
307
+ </span>
308
+ </div>
309
+ )}
310
+
311
+ {error && <div className={styles.error}>{error}</div>}
312
+
313
+ {!loading && artifacts.length === 0 && !error && (
314
+ <div className={styles.emptyState}>No artifacts found for current filters.</div>
315
+ )}
316
+
317
+ {artifacts.length > 0 && (
318
+ <div className={styles.contentArea}>
319
+ <div className={styles.tableContainer}>
320
+ <div className={styles.headerRow}>
321
+ <span>Timestamp</span>
322
+ <span>Type</span>
323
+ <span>Produced By</span>
324
+ <span>Correlation ID</span>
325
+ <span>Tags</span>
326
+ <span>Visibility</span>
327
+ <span>Consumed</span>
328
+ </div>
329
+ <div
330
+ ref={virtualScrollRef}
331
+ className={styles.virtualViewport}
332
+ style={{ height: listHeight }}
333
+ >
334
+ <div style={{ height: rows.length * ITEM_HEIGHT, position: 'relative' }}>
335
+ <div
336
+ style={{
337
+ position: 'absolute',
338
+ top: virtualRange.start * ITEM_HEIGHT,
339
+ left: 0,
340
+ right: 0,
341
+ }}
342
+ >
343
+ {rows.slice(virtualRange.start, virtualRange.end).map((row, idx) => {
344
+ const absoluteIndex = virtualRange.start + idx;
345
+ const isSelected = row.id === selectedArtifactId;
346
+ const classes = [styles.dataRow];
347
+ if (absoluteIndex % 2 === 1) {
348
+ classes.push(styles.dataRowStripe);
349
+ }
350
+ if (isSelected) {
351
+ classes.push(styles.dataRowSelected);
352
+ }
353
+ return (
354
+ <div
355
+ key={row.id}
356
+ className={classes.join(' ')}
357
+ style={{ height: ITEM_HEIGHT }}
358
+ role="button"
359
+ tabIndex={0}
360
+ aria-selected={isSelected}
361
+ onClick={() => setSelectedArtifactId(row.id)}
362
+ onKeyDown={(event) => {
363
+ if (event.key === 'Enter' || event.key === ' ') {
364
+ event.preventDefault();
365
+ setSelectedArtifactId(row.id);
366
+ }
367
+ }}
368
+ >
369
+ <span>{row.timestamp}</span>
370
+ <span>{row.type}</span>
371
+ <span>{row.producedBy}</span>
372
+ <span>{row.correlationId}</span>
373
+ <span>{row.tags}</span>
374
+ <span>{row.visibility}</span>
375
+ <span>{row.consumedCount}</span>
376
+ </div>
377
+ );
378
+ })}
379
+ </div>
380
+ </div>
381
+ </div>
382
+ </div>
383
+ <aside className={styles.detailPanel}>
384
+ {selectedArtifact ? (
385
+ <>
386
+ <div className={styles.detailHeader}>
387
+ <div>
388
+ <h3>{selectedArtifact.type}</h3>
389
+ <p>{new Date(selectedArtifact.created_at).toLocaleString()}</p>
390
+ </div>
391
+ <button type="button" onClick={() => setSelectedArtifactId(null)}>
392
+ Clear
393
+ </button>
394
+ </div>
395
+ <div className={styles.detailSection}>
396
+ <h4>Metadata</h4>
397
+ <dl className={styles.detailList}>
398
+ <div>
399
+ <dt>Produced By</dt>
400
+ <dd>{selectedArtifact.produced_by}</dd>
401
+ </div>
402
+ <div>
403
+ <dt>Correlation</dt>
404
+ <dd>{selectedArtifact.correlation_id || '—'}</dd>
405
+ </div>
406
+ <div>
407
+ <dt>Partition</dt>
408
+ <dd>{selectedArtifact.partition_key || '—'}</dd>
409
+ </div>
410
+ <div>
411
+ <dt>Tags</dt>
412
+ <dd>{selectedArtifact.tags.length ? selectedArtifact.tags.join(', ') : '—'}</dd>
413
+ </div>
414
+ <div>
415
+ <dt>Visibility</dt>
416
+ <dd>{selectedArtifact.visibility_kind || selectedArtifact.visibility?.kind || 'Unknown'}</dd>
417
+ </div>
418
+ <div>
419
+ <dt>Consumed By</dt>
420
+ <dd>{selectedArtifact.consumed_by?.length ? selectedArtifact.consumed_by.join(', ') : '—'}</dd>
421
+ </div>
422
+ </dl>
423
+ </div>
424
+ <div className={styles.detailSection}>
425
+ <h4>Payload</h4>
426
+ <JsonAttributeRenderer
427
+ value={JSON.stringify(selectedArtifact.payload, null, 2)}
428
+ maxStringLength={Number.POSITIVE_INFINITY}
429
+ />
430
+ </div>
431
+ <div className={styles.detailSection}>
432
+ <h4>Consumption History</h4>
433
+ {selectedArtifact.consumptions && selectedArtifact.consumptions.length > 0 ? (
434
+ <ul className={styles.consumptionList}>
435
+ {selectedArtifact.consumptions.map((entry) => (
436
+ <li key={`${entry.consumer}-${entry.consumed_at}`}>
437
+ <span className={styles.consumerName}>{entry.consumer}</span>
438
+ <span>{new Date(entry.consumed_at).toLocaleString()}</span>
439
+ {entry.run_id ? <span className={styles.runBadge}>Run {entry.run_id}</span> : null}
440
+ </li>
441
+ ))}
442
+ </ul>
443
+ ) : (
444
+ <p className={styles.emptyConsumption}>No consumers recorded for this artifact.</p>
445
+ )}
446
+ </div>
447
+ </>
448
+ ) : (
449
+ <div className={styles.emptyDetail}>Select an artifact to inspect payload and history.</div>
450
+ )}
451
+ </aside>
452
+ </div>
453
+ )}
454
+
455
+ {loading && <div className={styles.loading}>Loading…</div>}
456
+ </div>
457
+ );
458
+ };
459
+
460
+ export default HistoricalArtifactsModule;
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import type { ModuleContext } from './ModuleRegistry';
3
+ import HistoricalArtifactsModule from './HistoricalArtifactsModule';
4
+
5
+ interface HistoricalArtifactsModuleWrapperProps {
6
+ context: ModuleContext;
7
+ }
8
+
9
+ const HistoricalArtifactsModuleWrapper: React.FC<HistoricalArtifactsModuleWrapperProps> = ({ context }) => {
10
+ return <HistoricalArtifactsModule context={context} />;
11
+ };
12
+
13
+ export default HistoricalArtifactsModuleWrapper;
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { Agent, Message } from '../../types/graph';
3
- import type { TimeRange } from '../../types/filters';
3
+ import type { TimeRange, ArtifactSummary } from '../../types/filters';
4
4
 
5
5
  export interface ModuleContext {
6
6
  // Data access
@@ -12,8 +12,14 @@ export interface ModuleContext {
12
12
  filters: {
13
13
  correlationId: string | null;
14
14
  timeRange: TimeRange;
15
+ artifactTypes: string[];
16
+ producers: string[];
17
+ tags: string[];
18
+ visibility: string[];
15
19
  };
16
20
 
21
+ summary: ArtifactSummary | null;
22
+
17
23
  // Actions
18
24
  publish: (artifact: any) => void;
19
25
  invoke: (agentName: string, inputs: any[]) => void;
@@ -1,21 +1,12 @@
1
1
  import { moduleRegistry } from './ModuleRegistry';
2
- import EventLogModuleWrapper from './EventLogModuleWrapper';
3
2
  import TraceModuleJaegerWrapper from './TraceModuleJaegerWrapper';
3
+ import HistoricalArtifactsModuleWrapper from './HistoricalArtifactsModuleWrapper';
4
4
 
5
5
  /**
6
6
  * Register all available modules
7
7
  * This should be called during application initialization
8
8
  */
9
9
  export function registerModules(): void {
10
- // Register EventLog module
11
- moduleRegistry.register({
12
- id: 'eventLog',
13
- name: 'Event Log',
14
- description: 'View and filter system events',
15
- icon: '📋',
16
- component: EventLogModuleWrapper,
17
- });
18
-
19
10
  // Register Trace Viewer with Timeline, Statistics, RED Metrics, and Dependencies
20
11
  moduleRegistry.register({
21
12
  id: 'traceViewerJaeger',
@@ -25,6 +16,14 @@ export function registerModules(): void {
25
16
  component: TraceModuleJaegerWrapper,
26
17
  });
27
18
 
19
+ moduleRegistry.register({
20
+ id: 'historicalArtifacts',
21
+ name: 'Historical Blackboard',
22
+ description: 'Browse persisted artifacts and retention metrics',
23
+ icon: '📚',
24
+ component: HistoricalArtifactsModuleWrapper,
25
+ });
26
+
28
27
  // Future modules can be registered here
29
28
  // moduleRegistry.register({ ... });
30
29
  }
@@ -39,6 +39,11 @@ export function useModules() {
39
39
  const events = useGraphStore((state) => state.events);
40
40
  const correlationId = useFilterStore((state) => state.correlationId);
41
41
  const timeRange = useFilterStore((state) => state.timeRange);
42
+ const artifactTypes = useFilterStore((state) => state.selectedArtifactTypes);
43
+ const producers = useFilterStore((state) => state.selectedProducers);
44
+ const tags = useFilterStore((state) => state.selectedTags);
45
+ const visibility = useFilterStore((state) => state.selectedVisibility);
46
+ const summary = useFilterStore((state) => state.summary);
42
47
 
43
48
  // Track previous instances to detect changes
44
49
  const prevInstancesRef = useRef<Map<string, any>>(new Map());
@@ -52,7 +57,12 @@ export function useModules() {
52
57
  filters: {
53
58
  correlationId,
54
59
  timeRange,
60
+ artifactTypes,
61
+ producers,
62
+ tags,
63
+ visibility,
55
64
  },
65
+ summary,
56
66
  publish: (artifact: any) => {
57
67
  // Placeholder: In production, this would dispatch to WebSocket
58
68
  console.log('[Module Context] Publish artifact:', artifact);
@@ -62,7 +72,7 @@ export function useModules() {
62
72
  console.log('[Module Context] Invoke agent:', agentName, 'with inputs:', inputs);
63
73
  },
64
74
  }),
65
- [agents, messages, events, correlationId, timeRange]
75
+ [agents, messages, events, correlationId, timeRange, artifactTypes, producers, tags, visibility, summary]
66
76
  );
67
77
 
68
78
  /**