flock-core 0.5.0b70__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 +28 -9
  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.0b70.dist-info → flock_core-0.5.0b75.dist-info}/METADATA +1 -1
  53. {flock_core-0.5.0b70.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.0b70.dist-info → flock_core-0.5.0b75.dist-info}/WHEEL +0 -0
  61. {flock_core-0.5.0b70.dist-info → flock_core-0.5.0b75.dist-info}/entry_points.txt +0 -0
  62. {flock_core-0.5.0b70.dist-info → flock_core-0.5.0b75.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,6 @@
1
1
  import { useWSStore } from '../store/wsStore';
2
2
  import { useGraphStore } from '../store/graphStore';
3
3
  import { useFilterStore } from '../store/filterStore';
4
- import { useUIStore } from '../store/uiStore';
5
4
 
6
5
  interface WebSocketMessage {
7
6
  event_type: 'agent_activated' | 'message_published' | 'streaming_output' | 'agent_completed' | 'agent_error';
@@ -11,14 +10,6 @@ interface WebSocketMessage {
11
10
  data: any;
12
11
  }
13
12
 
14
- interface StoreInterface {
15
- addAgent: (agent: any) => void;
16
- updateAgent: (id: string, updates: any) => void;
17
- addMessage: (message: any) => void;
18
- updateMessage: (id: string, updates: any) => void;
19
- batchUpdate?: (update: any) => void;
20
- }
21
-
22
13
  export class WebSocketClient {
23
14
  ws: WebSocket | null = null;
24
15
  private reconnectTimeout: number | null = null;
@@ -34,18 +25,14 @@ export class WebSocketClient {
34
25
  private heartbeatInterval: number | null = null;
35
26
  private heartbeatTimeout: number | null = null;
36
27
  private connectionStatus: 'connecting' | 'connected' | 'disconnected' | 'disconnecting' | 'error' = 'disconnected';
37
- private store: StoreInterface;
38
28
  private enableHeartbeat: boolean;
39
29
 
40
- constructor(url: string, mockStore?: StoreInterface) {
30
+ // UI Optimization Migration (Phase 2 - Spec 002): Debounced graph refresh
31
+ private refreshTimer: number | null = null;
32
+ private refreshDebounceMs = 500; // 500ms batching window
33
+
34
+ constructor(url: string) {
41
35
  this.url = url;
42
- this.store = mockStore || {
43
- addAgent: (agent: any) => useGraphStore.getState().addAgent(agent),
44
- updateAgent: (id: string, updates: any) => useGraphStore.getState().updateAgent(id, updates),
45
- addMessage: (message: any) => useGraphStore.getState().addMessage(message),
46
- updateMessage: (id: string, updates: any) => useGraphStore.getState().updateMessage(id, updates),
47
- batchUpdate: (update: any) => useGraphStore.getState().batchUpdate(update),
48
- };
49
36
  // Phase 11 Fix: Disable heartbeat entirely - it causes unnecessary disconnects
50
37
  // WebSocket auto-reconnects on real network issues without needing heartbeat
51
38
  // The heartbeat was closing connections every 2min when backend didn't respond to pings
@@ -53,6 +40,26 @@ export class WebSocketClient {
53
40
  this.setupEventHandlers();
54
41
  }
55
42
 
43
+ /**
44
+ * UI Optimization Migration (Phase 2 - Spec 002): Debounced graph refresh
45
+ *
46
+ * Batch multiple graph-changing events within 500ms window, then fetch fresh
47
+ * snapshot from backend. This replaces immediate regenerateGraph() calls.
48
+ */
49
+ private scheduleGraphRefresh(): void {
50
+ if (this.refreshTimer !== null) {
51
+ clearTimeout(this.refreshTimer);
52
+ }
53
+
54
+ this.refreshTimer = window.setTimeout(() => {
55
+ this.refreshTimer = null;
56
+ // Call the NEW async refreshCurrentView() method
57
+ useGraphStore.getState().refreshCurrentView().catch((error) => {
58
+ console.error('[WebSocket] Graph refresh failed:', error);
59
+ });
60
+ }, this.refreshDebounceMs);
61
+ }
62
+
56
63
  private updateFilterStateFromPublishedMessage(data: any): void {
57
64
  const filterStore = useFilterStore.getState();
58
65
 
@@ -159,289 +166,116 @@ export class WebSocketClient {
159
166
  private setupEventHandlers(): void {
160
167
  // Handler for agent_activated: create/update agent in graph AND create Run
161
168
  this.on('agent_activated', (data) => {
162
- const agents = useGraphStore.getState().agents;
163
- const messages = useGraphStore.getState().messages;
164
- const existingAgent = agents.get(data.agent_id);
165
-
166
- // Count received messages by type
167
- const receivedByType = { ...(existingAgent?.receivedByType || {}) };
168
- if (data.consumed_artifacts && data.consumed_artifacts.length > 0) {
169
- // Look up each consumed artifact and count by type
170
- data.consumed_artifacts.forEach((artifactId: string) => {
171
- const message = messages.get(artifactId);
172
- if (message) {
173
- receivedByType[message.type] = (receivedByType[message.type] || 0) + 1;
174
- }
175
- });
176
- }
169
+ // UI Optimization Migration (Phase 2 - Spec 002): DEPRECATED client-side agent tracking
170
+ // Backend now handles all agent data. Frontend only tracks real-time status overlay.
171
+ // OLD CODE REMOVED: agents Map, receivedByType tracking, addAgent(), recordConsumption()
172
+ // NEW BEHAVIOR: Backend refresh will include updated agent data
177
173
 
178
- // Bug Fix #2: Preserve sentCount/recvCount if agent already exists
179
- // Otherwise counters get reset to 0 on each activation
180
- const agent = {
181
- id: data.agent_id,
182
- name: data.agent_name,
183
- status: 'running' as const,
184
- subscriptions: data.consumed_types || [],
185
- lastActive: Date.now(),
186
- sentCount: existingAgent?.sentCount || 0, // Preserve existing count
187
- recvCount: (existingAgent?.recvCount || 0) + (data.consumed_artifacts?.length || 0), // Add new consumed artifacts
188
- outputTypes: data.produced_types || [], // Get output types from backend
189
- receivedByType, // Track per-type received counts
190
- sentByType: existingAgent?.sentByType || {}, // Preserve sent counts
191
- };
192
- this.store.addAgent(agent);
174
+ // Update real-time status (fast, local)
175
+ useGraphStore.getState().updateAgentStatus(data.agent_name, 'running');
193
176
 
194
- // Phase 11 Bug Fix: Record actual consumption to track filtering
195
- // This enables showing "(3, filtered: 1)" on edges
196
- if (data.consumed_artifacts && data.consumed_artifacts.length > 0) {
197
- useGraphStore.getState().recordConsumption(data.consumed_artifacts, data.agent_id);
198
- }
199
-
200
- // Create Run object for Blackboard View edges
201
- // Bug Fix: Use run_id from backend (unique per agent activation) instead of correlation_id
202
- const run = {
203
- run_id: data.run_id || `run_${Date.now()}`, // data.run_id is ctx.task_id from backend
204
- agent_name: data.agent_name,
205
- correlation_id: data.correlation_id, // Separate field for grouping runs
206
- status: 'active' as const,
207
- consumed_artifacts: data.consumed_artifacts || [],
208
- produced_artifacts: [], // Will be populated on message_published
209
- started_at: new Date().toISOString(),
210
- };
211
- if (this.store.batchUpdate) {
212
- this.store.batchUpdate({ runs: [run] });
213
- }
177
+ // Schedule debounced refresh (batches within 500ms, then fetches backend snapshot)
178
+ this.scheduleGraphRefresh();
214
179
  });
215
180
 
216
181
  // Handler for message_published: update existing streaming message or create new one
217
182
  this.on('message_published', (data) => {
218
- // Finalize or create the message
219
- const messages = useGraphStore.getState().messages;
220
- const streamingMessageId = `streaming_${data.produced_by}_${data.correlation_id}`;
221
- const existingMessage = messages.get(streamingMessageId);
222
-
223
- if (existingMessage) {
224
- // Update existing streaming message with final data
225
- const tags = Array.isArray(data.tags) ? data.tags : [];
226
- const visibilityKind = data.visibility?.kind || data.visibility_kind || 'Unknown';
227
- const finalMessage = {
228
- ...existingMessage,
229
- id: data.artifact_id, // Replace temp ID with real artifact ID
230
- type: data.artifact_type,
231
- payload: data.payload,
232
- tags,
233
- visibilityKind,
234
- partitionKey: data.partition_key ?? null,
235
- version: data.version ?? 1,
236
- isStreaming: false, // Streaming complete
237
- streamingText: '', // Clear streaming text
238
- };
239
-
240
- // Use store action to properly update (triggers graph regeneration)
241
- useGraphStore.getState().finalizeStreamingMessage(streamingMessageId, finalMessage);
242
- } else {
243
- // No streaming message - create new message directly
244
- const tags = Array.isArray(data.tags) ? data.tags : [];
245
- const visibilityKind = data.visibility?.kind || data.visibility_kind || 'Unknown';
246
- const message = {
247
- id: data.artifact_id,
248
- type: data.artifact_type,
249
- payload: data.payload,
250
- timestamp: data.timestamp ? new Date(data.timestamp).getTime() : Date.now(),
251
- correlationId: data.correlation_id || '',
252
- producedBy: data.produced_by,
253
- tags,
254
- visibilityKind,
255
- partitionKey: data.partition_key ?? null,
256
- version: data.version ?? 1,
257
- isStreaming: false,
258
- };
259
- this.store.addMessage(message);
260
- }
261
-
262
- // Update producer agent counters (outputTypes come from agent_activated event now)
263
- const producer = useGraphStore.getState().agents.get(data.produced_by);
264
- if (producer) {
265
- // Track sent count by type
266
- const sentByType = { ...(producer.sentByType || {}) };
267
- sentByType[data.artifact_type] = (sentByType[data.artifact_type] || 0) + 1;
268
-
269
- this.store.updateAgent(data.produced_by, {
270
- sentCount: (producer.sentCount || 0) + 1,
271
- lastActive: Date.now(),
272
- sentByType,
273
- });
274
- } else {
275
- // Producer doesn't exist as a registered agent - create virtual agent
276
- // This handles orchestrator-published artifacts (e.g., initial Idea from dashboard PublishControl)
277
- this.store.addAgent({
278
- id: data.produced_by,
279
- name: data.produced_by,
280
- status: 'idle' as const,
281
- subscriptions: [],
282
- lastActive: Date.now(),
283
- sentCount: 1,
284
- recvCount: 0,
285
- outputTypes: [data.artifact_type], // Virtual agents get type from their first message
286
- sentByType: { [data.artifact_type]: 1 },
287
- receivedByType: {},
288
- });
289
- }
290
-
291
- // Phase 11 Bug Fix: Increment consumers' recv count instead of setting to 1
292
- if (data.consumers && Array.isArray(data.consumers)) {
293
- const agents = useGraphStore.getState().agents;
294
- data.consumers.forEach((consumerId: string) => {
295
- const consumer = agents.get(consumerId);
296
- if (consumer) {
297
- this.store.updateAgent(consumerId, {
298
- recvCount: (consumer.recvCount || 0) + 1,
299
- lastActive: Date.now(),
300
- });
301
- }
302
- });
303
- }
304
-
305
- // Update Run with produced artifact for Blackboard View edges
306
- // Bug Fix: Find Run by agent_name + correlation_id since run_id is not in message_published event
307
- if (data.correlation_id && this.store.batchUpdate) {
308
- const runs = useGraphStore.getState().runs;
309
- // Find the active Run for this agent + correlation_id
310
- const run = Array.from(runs.values()).find(
311
- r => r.agent_name === data.produced_by &&
312
- r.correlation_id === data.correlation_id &&
313
- r.status === 'active'
314
- );
315
- if (run) {
316
- // Add artifact to produced_artifacts if not already present
317
- if (!run.produced_artifacts.includes(data.artifact_id)) {
318
- const updatedRun = {
319
- ...run,
320
- produced_artifacts: [...run.produced_artifacts, data.artifact_id],
321
- };
322
- this.store.batchUpdate({ runs: [updatedRun] });
323
- }
324
- }
183
+ // UI Optimization Migration (Phase 2 - Spec 002): DEPRECATED client-side message tracking
184
+ // Backend now handles all message/artifact data. Frontend only tracks events for display.
185
+ // OLD CODE REMOVED: messages Map, addMessage(), updateMessage(), finalizeStreamingMessage(),
186
+ // agent counter updates, run tracking
187
+ // NEW BEHAVIOR: Backend refresh will include all updated data
188
+
189
+ // Phase 6: Finalize streaming message node if it exists
190
+ if (data.artifact_id) {
191
+ useGraphStore.getState().finalizeStreamingMessageNode(data.artifact_id);
325
192
  }
326
193
 
194
+ // Update filter state (still needed for filter UI)
327
195
  this.updateFilterStateFromPublishedMessage(data);
328
196
 
329
- // Ensure blackboard graph reflects the newly published artifact immediately
330
- const mode = useUIStore.getState().mode;
331
- if (mode === 'blackboard') {
332
- useGraphStore.getState().generateBlackboardViewGraph();
333
- } else if (mode === 'agent') {
334
- useGraphStore.getState().generateAgentViewGraph();
335
- }
197
+ // Add to events array for Event Log display
198
+ const message = {
199
+ id: data.artifact_id,
200
+ type: data.artifact_type,
201
+ payload: data.payload,
202
+ timestamp: data.timestamp ? new Date(data.timestamp).getTime() : Date.now(),
203
+ correlationId: data.correlation_id || '',
204
+ producedBy: data.produced_by,
205
+ tags: Array.isArray(data.tags) ? data.tags : [],
206
+ visibilityKind: data.visibility?.kind || data.visibility_kind || 'Unknown',
207
+ partitionKey: data.partition_key ?? null,
208
+ version: data.version ?? 1,
209
+ isStreaming: false,
210
+ };
211
+ useGraphStore.getState().addEvent(message);
212
+
213
+ // Schedule debounced refresh (batches multiple events within 500ms)
214
+ // This will replace the streaming node with the full backend snapshot
215
+ this.scheduleGraphRefresh();
336
216
  });
337
217
 
338
218
  // Handler for streaming_output: update live output (Phase 6)
339
219
  this.on('streaming_output', (data) => {
340
- // Phase 6: Update detail window live output
341
- console.log('[WebSocket] Streaming output:', data);
342
- // Update agent to show it's active and track streaming tokens for news ticker
220
+ // Phase 6: Only log start (sequence=0) and finish (is_final=true) to reduce noise
221
+ if (data.sequence === 0 || data.is_final) {
222
+ console.log('[WebSocket] Streaming output:', data.is_final ? 'FINAL' : 'START', data);
223
+ }
224
+
225
+ // Phase 6: Agent streaming tokens (for yellow ticker in agent nodes)
226
+ // Note: artifact_id is now always present (Phase 6), so we removed the !artifact_id check
343
227
  if (data.agent_name && data.output_type === 'llm_token') {
344
- const agents = useGraphStore.getState().agents;
345
- const agent = agents.get(data.agent_name);
346
- const currentTokens = agent?.streamingTokens || [];
228
+ const { streamingTokens } = useGraphStore.getState();
229
+ const currentTokens = streamingTokens.get(data.agent_name) || [];
347
230
 
348
231
  // Keep only last 6 tokens (news ticker effect)
349
232
  const updatedTokens = [...currentTokens, data.content].slice(-6);
350
233
 
351
- this.store.updateAgent(data.agent_name, {
352
- lastActive: Date.now(),
353
- streamingTokens: updatedTokens,
354
- });
234
+ useGraphStore.getState().updateStreamingTokens(data.agent_name, updatedTokens);
355
235
  }
356
236
 
357
- // Create/update streaming message node for blackboard view
358
- if (data.output_type === 'llm_token' && data.agent_name && data.correlation_id) {
359
- const messages = useGraphStore.getState().messages;
360
- // Use agent_name + correlation_id as temporary ID
361
- const streamingMessageId = `streaming_${data.agent_name}_${data.correlation_id}`;
362
- const existingMessage = messages.get(streamingMessageId);
363
-
364
- if (existingMessage) {
365
- // Append token to existing streaming message using updateMessage
366
- // This updates the messages Map without flooding the events array
367
- this.store.updateMessage(streamingMessageId, {
368
- streamingText: (existingMessage.streamingText || '') + data.content,
369
- timestamp: data.timestamp ? new Date(data.timestamp).getTime() : Date.now(),
370
- });
371
- } else if (data.sequence === 0 || !existingMessage) {
372
- // Look up agent's typical output type
373
- const agents = useGraphStore.getState().agents;
374
- const agent = agents.get(data.agent_name);
375
- const outputType = agent?.outputTypes?.[0] || 'output';
376
-
377
- // Create new streaming message on first token
378
- const streamingMessage = {
379
- id: streamingMessageId,
380
- type: outputType, // Use agent's known output type
381
- payload: {},
382
- timestamp: data.timestamp ? new Date(data.timestamp).getTime() : Date.now(),
383
- correlationId: data.correlation_id || '',
384
- producedBy: data.agent_name,
385
- tags: [],
386
- visibilityKind: 'Unknown',
387
- isStreaming: true,
388
- streamingText: data.content,
389
- };
390
- this.store.addMessage(streamingMessage);
237
+ // Phase 6: Message streaming preview (for streaming textbox in message nodes)
238
+ if (data.artifact_id && data.output_type === 'llm_token') {
239
+ // Create or update streaming message node
240
+ useGraphStore.getState().createOrUpdateStreamingMessageNode(
241
+ data.artifact_id,
242
+ data.content,
243
+ {
244
+ agent_name: data.agent_name,
245
+ correlation_id: data.correlation_id,
246
+ artifact_type: data.artifact_type, // Phase 6: Artifact type name for node header
247
+ }
248
+ );
249
+
250
+ // Finalize when streaming is complete (is_final=true)
251
+ if (data.is_final) {
252
+ useGraphStore.getState().finalizeStreamingMessageNode(data.artifact_id);
391
253
  }
392
254
  }
393
255
 
394
256
  // Note: The actual output storage is handled by LiveOutputTab's event listener
395
- // This handler is for store updates only
257
+ // This handler is for real-time token updates only
396
258
  });
397
259
 
398
260
  // Handler for agent_completed: update agent status to idle
399
261
  this.on('agent_completed', (data) => {
400
- this.store.updateAgent(data.agent_name, {
401
- status: 'idle',
402
- lastActive: Date.now(),
403
- streamingTokens: [], // Clear news ticker on completion
404
- });
262
+ // UI Optimization Migration (Phase 2 - Spec 002): Use NEW updateAgentStatus()
263
+ // for FAST real-time updates without backend calls
264
+ useGraphStore.getState().updateAgentStatus(data.agent_name, 'idle');
265
+ useGraphStore.getState().updateStreamingTokens(data.agent_name, []); // Clear news ticker
405
266
 
406
- // Update Run status to completed for Blackboard View edges
407
- // Bug Fix: Use run_id from event data (agent_completed has run_id)
408
- if (data.run_id && this.store.batchUpdate) {
409
- const runs = useGraphStore.getState().runs;
410
- const run = runs.get(data.run_id);
411
- if (run) {
412
- const updatedRun = {
413
- ...run,
414
- status: 'completed' as const,
415
- completed_at: new Date().toISOString(),
416
- duration_ms: data.duration_ms,
417
- };
418
- this.store.batchUpdate({ runs: [updatedRun] });
419
- }
420
- }
267
+ // OLD CODE REMOVED: Run status tracking (runs Map, batchUpdate)
268
+ // Backend handles run data now
421
269
  });
422
270
 
423
271
  // Handler for agent_error: update agent status to error
424
272
  this.on('agent_error', (data) => {
425
- this.store.updateAgent(data.agent_name, {
426
- status: 'error',
427
- lastActive: Date.now(),
428
- });
273
+ // UI Optimization Migration (Phase 2 - Spec 002): Use NEW updateAgentStatus()
274
+ // for FAST real-time updates without backend calls
275
+ useGraphStore.getState().updateAgentStatus(data.agent_name, 'error');
429
276
 
430
- // Update Run status to error
431
- // Bug Fix: Use run_id from event data (agent_error has run_id)
432
- if (data.run_id && this.store.batchUpdate) {
433
- const runs = useGraphStore.getState().runs;
434
- const run = runs.get(data.run_id);
435
- if (run) {
436
- const updatedRun = {
437
- ...run,
438
- status: 'error' as const,
439
- completed_at: new Date().toISOString(),
440
- error_message: data.error_message || 'Unknown error',
441
- };
442
- this.store.batchUpdate({ runs: [updatedRun] });
443
- }
444
- }
277
+ // OLD CODE REMOVED: Run status tracking (runs Map, batchUpdate)
278
+ // Backend handles run data now
445
279
  });
446
280
 
447
281
  // Handler for ping: respond with pong
@@ -602,12 +436,14 @@ export class WebSocketClient {
602
436
  let eventType = message.event_type;
603
437
  if (!eventType) {
604
438
  // Infer event type from data structure for test compatibility
439
+ // IMPORTANT: Check streaming_output BEFORE message_published since streaming events
440
+ // now have artifact_id + artifact_type (Phase 6) but also have run_id + output_type
605
441
  if (data.agent_id && data.consumed_types) {
606
442
  eventType = 'agent_activated';
607
- } else if (data.artifact_id && data.artifact_type) {
608
- eventType = 'message_published';
609
443
  } else if (data.run_id && data.output_type) {
610
444
  eventType = 'streaming_output';
445
+ } else if (data.artifact_id && data.artifact_type) {
446
+ eventType = 'message_published';
611
447
  } else if (data.run_id && data.duration_ms !== undefined) {
612
448
  eventType = 'agent_completed';
613
449
  } else if (data.run_id && data.error_type) {
@@ -1,6 +1,17 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
2
  import { useFilterStore } from './filterStore';
3
3
  import type { FilterFacets, FilterSnapshot } from '../types/filters';
4
+ import { useGraphStore } from './graphStore';
5
+
6
+ // Mock graphStore to test applyFilters integration
7
+ vi.mock('./graphStore', () => ({
8
+ useGraphStore: {
9
+ getState: vi.fn(() => ({
10
+ refreshCurrentView: vi.fn(),
11
+ viewMode: 'agent',
12
+ })),
13
+ },
14
+ }));
4
15
 
5
16
  describe('filterStore', () => {
6
17
  beforeEach(() => {
@@ -159,4 +170,81 @@ describe('filterStore', () => {
159
170
  expect(useFilterStore.getState().savedFilters).toHaveLength(0);
160
171
  });
161
172
  });
173
+
174
+ describe('applyFilters - backend integration (Phase 4)', () => {
175
+ beforeEach(() => {
176
+ vi.clearAllMocks();
177
+ });
178
+
179
+ it('should trigger backend snapshot refresh when applying filters', async () => {
180
+ const mockRefresh = vi.fn().mockResolvedValue(undefined);
181
+ vi.mocked(useGraphStore.getState).mockReturnValue({
182
+ refreshCurrentView: mockRefresh,
183
+ viewMode: 'agent',
184
+ } as any);
185
+
186
+ await useFilterStore.getState().applyFilters();
187
+
188
+ expect(mockRefresh).toHaveBeenCalledTimes(1);
189
+ });
190
+
191
+ it('should use current filter state when refreshing graph', async () => {
192
+ const mockRefresh = vi.fn().mockResolvedValue(undefined);
193
+ vi.mocked(useGraphStore.getState).mockReturnValue({
194
+ refreshCurrentView: mockRefresh,
195
+ viewMode: 'agent',
196
+ } as any);
197
+
198
+ // Set some filters
199
+ useFilterStore.setState({
200
+ correlationId: 'test-correlation-456',
201
+ selectedArtifactTypes: ['Pizza', 'Burger'],
202
+ selectedProducers: ['chef_agent'],
203
+ selectedTags: ['urgent'],
204
+ });
205
+
206
+ await useFilterStore.getState().applyFilters();
207
+
208
+ // Verify refresh was called (graphStore.refreshCurrentView will use current filter state)
209
+ expect(mockRefresh).toHaveBeenCalledTimes(1);
210
+
211
+ // The filters are passed via the filterStore state, which graphStore reads in buildGraphRequest
212
+ const filterState = useFilterStore.getState();
213
+ expect(filterState.correlationId).toBe('test-correlation-456');
214
+ expect(filterState.selectedArtifactTypes).toContain('Pizza');
215
+ });
216
+
217
+ it('should handle errors from backend refresh gracefully', async () => {
218
+ const mockRefresh = vi.fn().mockRejectedValue(new Error('Backend API error'));
219
+ vi.mocked(useGraphStore.getState).mockReturnValue({
220
+ refreshCurrentView: mockRefresh,
221
+ viewMode: 'agent',
222
+ } as any);
223
+
224
+ // Should propagate error to caller
225
+ await expect(useFilterStore.getState().applyFilters()).rejects.toThrow('Backend API error');
226
+ });
227
+
228
+ it('should work with both agent and blackboard view modes', async () => {
229
+ const mockRefresh = vi.fn().mockResolvedValue(undefined);
230
+
231
+ // Test agent view
232
+ vi.mocked(useGraphStore.getState).mockReturnValue({
233
+ refreshCurrentView: mockRefresh,
234
+ viewMode: 'agent',
235
+ } as any);
236
+
237
+ await useFilterStore.getState().applyFilters();
238
+ expect(mockRefresh).toHaveBeenCalledTimes(1);
239
+
240
+ // Test blackboard view
241
+ vi.mocked(useGraphStore.getState).mockReturnValue({
242
+ refreshCurrentView: mockRefresh,
243
+ viewMode: 'blackboard',
244
+ } as any);
245
+
246
+ await useFilterStore.getState().applyFilters();
247
+ expect(mockRefresh).toHaveBeenCalledTimes(2);
248
+ });
249
+ });
162
250
  });
@@ -1,5 +1,5 @@
1
1
  import { create } from 'zustand';
2
- import { devtools } from 'zustand/middleware';
2
+ import { devtools, persist } from 'zustand/middleware';
3
3
  import {
4
4
  TimeRange,
5
5
  CorrelationIdMetadata,
@@ -8,6 +8,7 @@ import {
8
8
  SavedFilterMeta,
9
9
  FilterSnapshot,
10
10
  } from '../types/filters';
11
+ import { useGraphStore } from './graphStore';
11
12
 
12
13
  export type ActiveFilterType =
13
14
  | 'correlationId'
@@ -45,6 +46,7 @@ interface FilterState {
45
46
  setTags: (tags: string[]) => void;
46
47
  setVisibility: (visibility: string[]) => void;
47
48
  clearFilters: () => void;
49
+ applyFilters: () => Promise<void>;
48
50
 
49
51
  updateAvailableCorrelationIds: (metadata: CorrelationIdMetadata[]) => void;
50
52
  updateAvailableFacets: (facets: FilterFacets) => void;
@@ -82,20 +84,21 @@ const uniqueSorted = (items: string[]) => Array.from(new Set(items)).sort((a, b)
82
84
 
83
85
  export const useFilterStore = create<FilterState>()(
84
86
  devtools(
85
- (set, get) => ({
86
- correlationId: null,
87
- timeRange: defaultTimeRange,
88
- selectedArtifactTypes: [],
89
- selectedProducers: [],
90
- selectedTags: [],
91
- selectedVisibility: [],
92
- availableCorrelationIds: [],
93
- availableArtifactTypes: [],
94
- availableProducers: [],
95
- availableTags: [],
96
- availableVisibility: [],
97
- summary: null,
98
- savedFilters: [],
87
+ persist(
88
+ (set, get) => ({
89
+ correlationId: null,
90
+ timeRange: defaultTimeRange,
91
+ selectedArtifactTypes: [],
92
+ selectedProducers: [],
93
+ selectedTags: [],
94
+ selectedVisibility: [],
95
+ availableCorrelationIds: [],
96
+ availableArtifactTypes: [],
97
+ availableProducers: [],
98
+ availableTags: [],
99
+ availableVisibility: [],
100
+ summary: null,
101
+ savedFilters: [],
99
102
 
100
103
  setCorrelationId: (id) => set({ correlationId: id }),
101
104
  setTimeRange: (range) => set({ timeRange: range }),
@@ -113,6 +116,13 @@ export const useFilterStore = create<FilterState>()(
113
116
  selectedVisibility: [],
114
117
  }),
115
118
 
119
+ applyFilters: async () => {
120
+ // UI Optimization Migration (Phase 4 - Spec 002): Backend-driven filtering
121
+ // Trigger backend snapshot refresh with current filter state
122
+ // The graphStore will read current filter state via buildGraphRequest()
123
+ await useGraphStore.getState().refreshCurrentView();
124
+ },
125
+
116
126
  updateAvailableCorrelationIds: (metadata) => {
117
127
  const sorted = [...metadata].sort((a, b) => b.first_seen - a.first_seen);
118
128
  set({
@@ -244,7 +254,19 @@ export const useFilterStore = create<FilterState>()(
244
254
  return {};
245
255
  });
246
256
  },
247
- }),
257
+ }),
258
+ {
259
+ name: 'flock-filter-state',
260
+ partialize: (state) => ({
261
+ correlationId: state.correlationId,
262
+ timeRange: state.timeRange,
263
+ selectedArtifactTypes: state.selectedArtifactTypes,
264
+ selectedProducers: state.selectedProducers,
265
+ selectedTags: state.selectedTags,
266
+ selectedVisibility: state.selectedVisibility,
267
+ }),
268
+ }
269
+ ),
248
270
  { name: 'filterStore' }
249
271
  )
250
272
  );