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.
- flock/agent.py +39 -1
- flock/artifacts.py +17 -10
- flock/cli.py +1 -1
- flock/dashboard/__init__.py +2 -0
- flock/dashboard/collector.py +282 -6
- flock/dashboard/events.py +6 -0
- flock/dashboard/graph_builder.py +563 -0
- flock/dashboard/launcher.py +11 -6
- flock/dashboard/models/graph.py +156 -0
- flock/dashboard/service.py +175 -14
- flock/dashboard/static_v2/assets/index-DFRnI_mt.js +111 -0
- flock/dashboard/static_v2/assets/index-fPLNdmp1.css +1 -0
- flock/dashboard/static_v2/index.html +13 -0
- flock/dashboard/websocket.py +2 -2
- flock/engines/dspy_engine.py +27 -8
- flock/frontend/README.md +6 -6
- flock/frontend/src/App.tsx +23 -31
- flock/frontend/src/__tests__/integration/graph-snapshot.test.tsx +647 -0
- flock/frontend/src/components/details/DetailWindowContainer.tsx +13 -17
- flock/frontend/src/components/details/MessageDetailWindow.tsx +439 -0
- flock/frontend/src/components/details/MessageHistoryTab.tsx +128 -53
- flock/frontend/src/components/details/RunStatusTab.tsx +79 -38
- flock/frontend/src/components/graph/AgentNode.test.tsx +3 -1
- flock/frontend/src/components/graph/AgentNode.tsx +8 -6
- flock/frontend/src/components/graph/GraphCanvas.tsx +13 -8
- flock/frontend/src/components/graph/MessageNode.test.tsx +3 -1
- flock/frontend/src/components/graph/MessageNode.tsx +16 -3
- flock/frontend/src/components/layout/DashboardLayout.tsx +12 -9
- flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +4 -14
- flock/frontend/src/components/modules/ModuleRegistry.ts +5 -3
- flock/frontend/src/hooks/useModules.ts +12 -4
- flock/frontend/src/hooks/usePersistence.ts +5 -3
- flock/frontend/src/services/api.ts +3 -19
- flock/frontend/src/services/graphService.test.ts +330 -0
- flock/frontend/src/services/graphService.ts +75 -0
- flock/frontend/src/services/websocket.ts +104 -268
- flock/frontend/src/store/filterStore.test.ts +89 -1
- flock/frontend/src/store/filterStore.ts +38 -16
- flock/frontend/src/store/graphStore.test.ts +538 -173
- flock/frontend/src/store/graphStore.ts +374 -465
- flock/frontend/src/store/moduleStore.ts +51 -33
- flock/frontend/src/store/uiStore.ts +23 -11
- flock/frontend/src/types/graph.ts +77 -44
- flock/frontend/src/utils/mockData.ts +16 -3
- flock/frontend/vite.config.ts +2 -2
- flock/orchestrator.py +24 -6
- flock/service.py +2 -2
- flock/store.py +169 -4
- flock/themes/darkmatrix.toml +2 -2
- flock/themes/deep.toml +2 -2
- flock/themes/neopolitan.toml +4 -4
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/METADATA +1 -1
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/RECORD +56 -53
- flock/frontend/src/__tests__/e2e/critical-scenarios.test.tsx +0 -586
- flock/frontend/src/__tests__/integration/filtering-e2e.test.tsx +0 -391
- flock/frontend/src/__tests__/integration/graph-rendering.test.tsx +0 -640
- flock/frontend/src/services/websocket.test.ts +0 -595
- flock/frontend/src/utils/transforms.test.ts +0 -860
- flock/frontend/src/utils/transforms.ts +0 -323
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/WHEEL +0 -0
- {flock_core-0.5.0b71.dist-info → flock_core-0.5.0b75.dist-info}/entry_points.txt +0 -0
- {flock_core-0.5.0b71.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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
//
|
|
179
|
-
|
|
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
|
-
//
|
|
195
|
-
|
|
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
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
//
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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:
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
345
|
-
const
|
|
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
|
-
|
|
352
|
-
lastActive: Date.now(),
|
|
353
|
-
streamingTokens: updatedTokens,
|
|
354
|
-
});
|
|
234
|
+
useGraphStore.getState().updateStreamingTokens(data.agent_name, updatedTokens);
|
|
355
235
|
}
|
|
356
236
|
|
|
357
|
-
//
|
|
358
|
-
if (data.output_type === 'llm_token'
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
//
|
|
407
|
-
//
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
//
|
|
431
|
-
//
|
|
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
|
-
(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
);
|