flock-core 0.5.0b56__py3-none-any.whl → 0.5.0b58__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.

@@ -103,9 +103,9 @@ class WebSocketManager:
103
103
  # Store streaming output events for history (always, even if no clients)
104
104
  if isinstance(event, StreamingOutputEvent):
105
105
  self._streaming_history[event.agent_name].append(event)
106
- logger.debug(
107
- f"Stored streaming event for {event.agent_name}, history size: {len(self._streaming_history[event.agent_name])}"
108
- )
106
+ # logger.debug(
107
+ # f"Stored streaming event for {event.agent_name}, history size: {len(self._streaming_history[event.agent_name])}"
108
+ # )
109
109
 
110
110
  # If no clients, still log but don't broadcast
111
111
  if not self.clients:
@@ -115,11 +115,11 @@ class WebSocketManager:
115
115
  return
116
116
 
117
117
  # Log broadcast attempt
118
- logger.debug(f"Broadcasting {type(event).__name__} to {len(self.clients)} client(s)")
118
+ # logger.debug(f"Broadcasting {type(event).__name__} to {len(self.clients)} client(s)")
119
119
 
120
120
  # Serialize event to JSON using Pydantic's model_dump_json
121
121
  message = event.model_dump_json()
122
- logger.debug(f"Event JSON: {message[:200]}...") # Log first 200 chars
122
+ # logger.debug(f"Event JSON: {message[:200]}...") # Log first 200 chars
123
123
 
124
124
  # Broadcast to all clients concurrently
125
125
  # Use return_exceptions=True to handle client failures gracefully
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "flock-ui",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "flock-ui",
9
- "version": "0.1.2",
9
+ "version": "0.1.3",
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@types/dagre": "^0.7.53",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flock-ui",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Flock Flow Real-Time Dashboard Frontend",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -21,16 +21,20 @@ import { useUIStore } from '../../store/uiStore';
21
21
  import { useModuleStore } from '../../store/moduleStore';
22
22
  import { useSettingsStore } from '../../store/settingsStore';
23
23
  import { moduleRegistry } from '../modules/ModuleRegistry';
24
- import { applyDagreLayout } from '../../services/layout';
24
+ import {
25
+ applyHierarchicalLayout,
26
+ applyCircularLayout,
27
+ applyGridLayout,
28
+ applyRandomLayout
29
+ } from '../../services/layout';
25
30
  import { usePersistence } from '../../hooks/usePersistence';
26
31
  import { v4 as uuidv4 } from 'uuid';
27
32
 
28
33
  const GraphCanvas: React.FC = () => {
29
- const { fitView, getIntersectingNodes } = useReactFlow();
34
+ const { fitView, getIntersectingNodes, screenToFlowPosition } = useReactFlow();
30
35
 
31
36
  const mode = useUIStore((state) => state.mode);
32
37
  const openDetailWindow = useUIStore((state) => state.openDetailWindow);
33
- const layoutDirection = useSettingsStore((state) => state.advanced.layoutDirection);
34
38
  const nodes = useGraphStore((state) => state.nodes);
35
39
  const edges = useGraphStore((state) => state.edges);
36
40
  const agents = useGraphStore((state) => state.agents);
@@ -48,6 +52,7 @@ const GraphCanvas: React.FC = () => {
48
52
 
49
53
  const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
50
54
  const [showModuleSubmenu, setShowModuleSubmenu] = useState(false);
55
+ const [showLayoutSubmenu, setShowLayoutSubmenu] = useState(false);
51
56
 
52
57
  // Persistence hook - loads positions on mount and handles saves
53
58
  const { saveNodePosition } = usePersistence();
@@ -111,21 +116,52 @@ const GraphCanvas: React.FC = () => {
111
116
  [edges]
112
117
  );
113
118
 
114
- // Auto-layout handler
115
- const handleAutoLayout = useCallback(() => {
116
- const nodeSpacing = useSettingsStore.getState().advanced.nodeSpacing;
117
- const rankSpacing = useSettingsStore.getState().advanced.rankSpacing;
118
- const layoutedNodes = applyDagreLayout(nodes, edges, layoutDirection || 'TB', nodeSpacing, rankSpacing);
119
+ // Generic layout handler
120
+ const applyLayout = useCallback((layoutType: string) => {
121
+ // Get the React Flow pane element to find its actual center
122
+ const pane = document.querySelector('.react-flow__pane');
123
+ let viewportCenter = { x: 0, y: 0 };
124
+
125
+ if (pane) {
126
+ const rect = pane.getBoundingClientRect();
127
+ // Convert screen center of the pane to flow coordinates
128
+ viewportCenter = screenToFlowPosition({
129
+ x: rect.left + rect.width / 2,
130
+ y: rect.top + rect.height / 2,
131
+ });
132
+ }
133
+
134
+ let result;
135
+ switch (layoutType) {
136
+ case 'hierarchical-vertical':
137
+ result = applyHierarchicalLayout(nodes, edges, { direction: 'TB', center: viewportCenter });
138
+ break;
139
+ case 'hierarchical-horizontal':
140
+ result = applyHierarchicalLayout(nodes, edges, { direction: 'LR', center: viewportCenter });
141
+ break;
142
+ case 'circular':
143
+ result = applyCircularLayout(nodes, edges, { center: viewportCenter });
144
+ break;
145
+ case 'grid':
146
+ result = applyGridLayout(nodes, edges, { center: viewportCenter });
147
+ break;
148
+ case 'random':
149
+ result = applyRandomLayout(nodes, edges, { center: viewportCenter });
150
+ break;
151
+ default:
152
+ result = applyHierarchicalLayout(nodes, edges, { direction: 'TB', center: viewportCenter });
153
+ }
119
154
 
120
155
  // Update nodes with new positions
121
- layoutedNodes.forEach((node) => {
156
+ result.nodes.forEach((node) => {
122
157
  updateNodePosition(node.id, node.position);
123
158
  });
124
159
 
125
- useGraphStore.setState({ nodes: layoutedNodes });
160
+ useGraphStore.setState({ nodes: result.nodes });
126
161
  setContextMenu(null);
127
162
  setShowModuleSubmenu(false);
128
- }, [nodes, edges, layoutDirection, updateNodePosition]);
163
+ setShowLayoutSubmenu(false);
164
+ }, [nodes, edges, updateNodePosition, screenToFlowPosition]);
129
165
 
130
166
  // Auto-zoom handler
131
167
  const handleAutoZoom = useCallback(() => {
@@ -163,6 +199,7 @@ const GraphCanvas: React.FC = () => {
163
199
  const onPaneClick = useCallback(() => {
164
200
  setContextMenu(null);
165
201
  setShowModuleSubmenu(false);
202
+ setShowLayoutSubmenu(false);
166
203
  }, []);
167
204
 
168
205
  // Node drag handler - prevent overlaps with collision detection
@@ -270,29 +307,170 @@ const GraphCanvas: React.FC = () => {
270
307
  minWidth: 180,
271
308
  }}
272
309
  >
273
- <button
274
- onClick={handleAutoLayout}
275
- style={{
276
- display: 'block',
277
- width: '100%',
278
- padding: 'var(--spacing-2) var(--spacing-4)',
279
- border: 'none',
280
- background: 'transparent',
281
- cursor: 'pointer',
282
- textAlign: 'left',
283
- fontSize: 'var(--font-size-body-sm)',
284
- color: 'var(--color-text-primary)',
285
- transition: 'var(--transition-colors)',
286
- }}
287
- onMouseEnter={(e) => {
288
- e.currentTarget.style.background = 'var(--color-bg-overlay)';
289
- }}
290
- onMouseLeave={(e) => {
291
- e.currentTarget.style.background = 'transparent';
292
- }}
293
- >
294
- Auto Layout
295
- </button>
310
+ <div style={{ position: 'relative' }}>
311
+ <button
312
+ onMouseEnter={() => setShowLayoutSubmenu(true)}
313
+ onMouseLeave={(e) => {
314
+ const relatedTarget = e.relatedTarget as HTMLElement;
315
+ if (!relatedTarget || !relatedTarget.closest('.layout-submenu')) {
316
+ setShowLayoutSubmenu(false);
317
+ }
318
+ }}
319
+ style={{
320
+ display: 'flex',
321
+ alignItems: 'center',
322
+ justifyContent: 'space-between',
323
+ width: '100%',
324
+ padding: 'var(--spacing-2) var(--spacing-4)',
325
+ border: 'none',
326
+ background: showLayoutSubmenu ? 'var(--color-bg-overlay)' : 'transparent',
327
+ cursor: 'pointer',
328
+ textAlign: 'left',
329
+ fontSize: 'var(--font-size-body-sm)',
330
+ color: 'var(--color-text-primary)',
331
+ transition: 'var(--transition-colors)',
332
+ }}
333
+ >
334
+ <span>Auto Layout</span>
335
+ <span style={{ marginLeft: 'var(--spacing-2)' }}>▶</span>
336
+ </button>
337
+
338
+ {/* Layout Submenu */}
339
+ {showLayoutSubmenu && (
340
+ <div
341
+ className="layout-submenu"
342
+ onMouseEnter={() => setShowLayoutSubmenu(true)}
343
+ onMouseLeave={() => setShowLayoutSubmenu(false)}
344
+ style={{
345
+ position: 'absolute',
346
+ top: 0,
347
+ left: '100%',
348
+ background: 'var(--color-bg-surface)',
349
+ border: 'var(--border-default)',
350
+ borderRadius: 'var(--radius-md)',
351
+ boxShadow: 'var(--shadow-lg)',
352
+ zIndex: 1001,
353
+ minWidth: 180,
354
+ }}
355
+ >
356
+ <button
357
+ onClick={() => applyLayout('hierarchical-vertical')}
358
+ style={{
359
+ display: 'block',
360
+ width: '100%',
361
+ padding: 'var(--spacing-2) var(--spacing-4)',
362
+ border: 'none',
363
+ background: 'transparent',
364
+ cursor: 'pointer',
365
+ textAlign: 'left',
366
+ fontSize: 'var(--font-size-body-sm)',
367
+ color: 'var(--color-text-primary)',
368
+ transition: 'var(--transition-colors)',
369
+ }}
370
+ onMouseEnter={(e) => {
371
+ e.currentTarget.style.background = 'var(--color-bg-overlay)';
372
+ }}
373
+ onMouseLeave={(e) => {
374
+ e.currentTarget.style.background = 'transparent';
375
+ }}
376
+ >
377
+ Hierarchical (Vertical)
378
+ </button>
379
+ <button
380
+ onClick={() => applyLayout('hierarchical-horizontal')}
381
+ style={{
382
+ display: 'block',
383
+ width: '100%',
384
+ padding: 'var(--spacing-2) var(--spacing-4)',
385
+ border: 'none',
386
+ background: 'transparent',
387
+ cursor: 'pointer',
388
+ textAlign: 'left',
389
+ fontSize: 'var(--font-size-body-sm)',
390
+ color: 'var(--color-text-primary)',
391
+ transition: 'var(--transition-colors)',
392
+ }}
393
+ onMouseEnter={(e) => {
394
+ e.currentTarget.style.background = 'var(--color-bg-overlay)';
395
+ }}
396
+ onMouseLeave={(e) => {
397
+ e.currentTarget.style.background = 'transparent';
398
+ }}
399
+ >
400
+ Hierarchical (Horizontal)
401
+ </button>
402
+ <button
403
+ onClick={() => applyLayout('circular')}
404
+ style={{
405
+ display: 'block',
406
+ width: '100%',
407
+ padding: 'var(--spacing-2) var(--spacing-4)',
408
+ border: 'none',
409
+ background: 'transparent',
410
+ cursor: 'pointer',
411
+ textAlign: 'left',
412
+ fontSize: 'var(--font-size-body-sm)',
413
+ color: 'var(--color-text-primary)',
414
+ transition: 'var(--transition-colors)',
415
+ }}
416
+ onMouseEnter={(e) => {
417
+ e.currentTarget.style.background = 'var(--color-bg-overlay)';
418
+ }}
419
+ onMouseLeave={(e) => {
420
+ e.currentTarget.style.background = 'transparent';
421
+ }}
422
+ >
423
+ Circular
424
+ </button>
425
+ <button
426
+ onClick={() => applyLayout('grid')}
427
+ style={{
428
+ display: 'block',
429
+ width: '100%',
430
+ padding: 'var(--spacing-2) var(--spacing-4)',
431
+ border: 'none',
432
+ background: 'transparent',
433
+ cursor: 'pointer',
434
+ textAlign: 'left',
435
+ fontSize: 'var(--font-size-body-sm)',
436
+ color: 'var(--color-text-primary)',
437
+ transition: 'var(--transition-colors)',
438
+ }}
439
+ onMouseEnter={(e) => {
440
+ e.currentTarget.style.background = 'var(--color-bg-overlay)';
441
+ }}
442
+ onMouseLeave={(e) => {
443
+ e.currentTarget.style.background = 'transparent';
444
+ }}
445
+ >
446
+ Grid
447
+ </button>
448
+ <button
449
+ onClick={() => applyLayout('random')}
450
+ style={{
451
+ display: 'block',
452
+ width: '100%',
453
+ padding: 'var(--spacing-2) var(--spacing-4)',
454
+ border: 'none',
455
+ background: 'transparent',
456
+ cursor: 'pointer',
457
+ textAlign: 'left',
458
+ fontSize: 'var(--font-size-body-sm)',
459
+ color: 'var(--color-text-primary)',
460
+ transition: 'var(--transition-colors)',
461
+ }}
462
+ onMouseEnter={(e) => {
463
+ e.currentTarget.style.background = 'var(--color-bg-overlay)';
464
+ }}
465
+ onMouseLeave={(e) => {
466
+ e.currentTarget.style.background = 'transparent';
467
+ }}
468
+ >
469
+ Random
470
+ </button>
471
+ </div>
472
+ )}
473
+ </div>
296
474
 
297
475
  <button
298
476
  onClick={handleAutoZoom}
@@ -15,6 +15,7 @@ export interface LayoutOptions {
15
15
  direction?: 'TB' | 'LR' | 'BT' | 'RL';
16
16
  nodeSpacing?: number;
17
17
  rankSpacing?: number;
18
+ center?: { x: number; y: number }; // Optional center point for layout
18
19
  }
19
20
 
20
21
  export interface LayoutResult {
@@ -30,10 +31,6 @@ const DEFAULT_NODE_HEIGHT = 80;
30
31
  const MESSAGE_NODE_WIDTH = 150;
31
32
  const MESSAGE_NODE_HEIGHT = 60;
32
33
 
33
- // Default spacing (increased by 50% for better label visibility)
34
- const DEFAULT_NODE_SPACING = 75; // Was 50
35
- const DEFAULT_RANK_SPACING = 150; // Was 100
36
-
37
34
  /**
38
35
  * Get node dimensions based on node type
39
36
  */
@@ -59,8 +56,7 @@ export function applyHierarchicalLayout(
59
56
  ): LayoutResult {
60
57
  const {
61
58
  direction = 'TB',
62
- nodeSpacing = DEFAULT_NODE_SPACING,
63
- rankSpacing = DEFAULT_RANK_SPACING,
59
+ center,
64
60
  } = options;
65
61
 
66
62
  // Handle empty graph
@@ -68,6 +64,21 @@ export function applyHierarchicalLayout(
68
64
  return { nodes: [], edges, width: 0, height: 0 };
69
65
  }
70
66
 
67
+ // Calculate dynamic spacing based on actual node sizes
68
+ // This ensures 200px minimum clearance regardless of node dimensions
69
+ let maxWidth = 0;
70
+ let maxHeight = 0;
71
+
72
+ nodes.forEach((node) => {
73
+ const { width, height } = getNodeDimensions(node);
74
+ maxWidth = Math.max(maxWidth, width);
75
+ maxHeight = Math.max(maxHeight, height);
76
+ });
77
+
78
+ // Spacing = half of max node size + 200px minimum clearance
79
+ const nodeSpacing = options.nodeSpacing ?? (maxWidth / 2 + 200);
80
+ const rankSpacing = options.rankSpacing ?? (maxHeight / 2 + 200);
81
+
71
82
  // Create a new directed graph
72
83
  const graph = new dagre.graphlib.Graph();
73
84
 
@@ -97,6 +108,15 @@ export function applyHierarchicalLayout(
97
108
  // Run the layout algorithm
98
109
  dagre.layout(graph);
99
110
 
111
+ // Get graph dimensions first to calculate offset
112
+ const graphConfig = graph.graph();
113
+ const graphWidth = (graphConfig.width || 0) + 40; // Add margin
114
+ const graphHeight = (graphConfig.height || 0) + 40; // Add margin
115
+
116
+ // Calculate offset to center the layout around viewport center (or 0,0 if no center provided)
117
+ const offsetX = center ? center.x - graphWidth / 2 : 0;
118
+ const offsetY = center ? center.y - graphHeight / 2 : 0;
119
+
100
120
  // Extract positioned nodes
101
121
  const layoutedNodes = nodes.map((node) => {
102
122
  const nodeWithPosition = graph.node(node.id);
@@ -107,22 +127,211 @@ export function applyHierarchicalLayout(
107
127
  return {
108
128
  ...node,
109
129
  position: {
110
- x: nodeWithPosition.x - width / 2,
111
- y: nodeWithPosition.y - height / 2,
130
+ x: nodeWithPosition.x - width / 2 + offsetX,
131
+ y: nodeWithPosition.y - height / 2 + offsetY,
112
132
  },
113
133
  };
114
134
  });
115
135
 
116
- // Get graph dimensions
117
- const graphConfig = graph.graph();
118
- const width = (graphConfig.width || 0) + 40; // Add margin
119
- const height = (graphConfig.height || 0) + 40; // Add margin
136
+ return {
137
+ nodes: layoutedNodes,
138
+ edges,
139
+ width: graphWidth,
140
+ height: graphHeight,
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Apply circular layout - nodes arranged in a circle
146
+ */
147
+ export function applyCircularLayout(
148
+ nodes: Node[],
149
+ edges: Edge[],
150
+ options: LayoutOptions = {}
151
+ ): LayoutResult {
152
+ const { center } = options;
153
+
154
+ if (nodes.length === 0) {
155
+ return { nodes: [], edges, width: 0, height: 0 };
156
+ }
157
+
158
+ // Calculate radius based on number of nodes and their sizes
159
+ let maxWidth = 0;
160
+ let maxHeight = 0;
161
+ nodes.forEach((node) => {
162
+ const { width, height } = getNodeDimensions(node);
163
+ maxWidth = Math.max(maxWidth, width);
164
+ maxHeight = Math.max(maxHeight, height);
165
+ });
166
+
167
+ const minSpacing = 200; // 200px minimum clearance
168
+ const nodeSize = Math.max(maxWidth, maxHeight);
169
+ const circumference = nodes.length * (nodeSize + minSpacing);
170
+ const radius = circumference / (2 * Math.PI);
171
+
172
+ const centerX = center?.x ?? 0;
173
+ const centerY = center?.y ?? 0;
174
+
175
+ const layoutedNodes = nodes.map((node, index) => {
176
+ const angle = (2 * Math.PI * index) / nodes.length;
177
+ const { width, height } = getNodeDimensions(node);
178
+
179
+ return {
180
+ ...node,
181
+ position: {
182
+ x: centerX + radius * Math.cos(angle) - width / 2,
183
+ y: centerY + radius * Math.sin(angle) - height / 2,
184
+ },
185
+ };
186
+ });
187
+
188
+ const graphWidth = radius * 2 + maxWidth;
189
+ const graphHeight = radius * 2 + maxHeight;
190
+
191
+ return {
192
+ nodes: layoutedNodes,
193
+ edges,
194
+ width: graphWidth,
195
+ height: graphHeight,
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Apply grid layout - nodes arranged in a grid
201
+ */
202
+ export function applyGridLayout(
203
+ nodes: Node[],
204
+ edges: Edge[],
205
+ options: LayoutOptions = {}
206
+ ): LayoutResult {
207
+ const { center } = options;
208
+
209
+ if (nodes.length === 0) {
210
+ return { nodes: [], edges, width: 0, height: 0 };
211
+ }
212
+
213
+ // Calculate grid dimensions
214
+ const cols = Math.ceil(Math.sqrt(nodes.length));
215
+ const rows = Math.ceil(nodes.length / cols);
216
+
217
+ let maxWidth = 0;
218
+ let maxHeight = 0;
219
+ nodes.forEach((node) => {
220
+ const { width, height } = getNodeDimensions(node);
221
+ maxWidth = Math.max(maxWidth, width);
222
+ maxHeight = Math.max(maxHeight, height);
223
+ });
224
+
225
+ const minSpacing = 200;
226
+ const cellWidth = maxWidth + minSpacing;
227
+ const cellHeight = maxHeight + minSpacing;
228
+
229
+ const graphWidth = cols * cellWidth;
230
+ const graphHeight = rows * cellHeight;
231
+
232
+ const startX = center ? center.x - graphWidth / 2 : 0;
233
+ const startY = center ? center.y - graphHeight / 2 : 0;
234
+
235
+ const layoutedNodes = nodes.map((node, index) => {
236
+ const col = index % cols;
237
+ const row = Math.floor(index / cols);
238
+ const { width, height } = getNodeDimensions(node);
239
+
240
+ return {
241
+ ...node,
242
+ position: {
243
+ x: startX + col * cellWidth + (cellWidth - width) / 2,
244
+ y: startY + row * cellHeight + (cellHeight - height) / 2,
245
+ },
246
+ };
247
+ });
248
+
249
+ return {
250
+ nodes: layoutedNodes,
251
+ edges,
252
+ width: graphWidth,
253
+ height: graphHeight,
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Apply random layout - nodes placed randomly with minimum spacing
259
+ */
260
+ export function applyRandomLayout(
261
+ nodes: Node[],
262
+ edges: Edge[],
263
+ options: LayoutOptions = {}
264
+ ): LayoutResult {
265
+ const { center } = options;
266
+
267
+ if (nodes.length === 0) {
268
+ return { nodes: [], edges, width: 0, height: 0 };
269
+ }
270
+
271
+ let maxWidth = 0;
272
+ let maxHeight = 0;
273
+ nodes.forEach((node) => {
274
+ const { width, height } = getNodeDimensions(node);
275
+ maxWidth = Math.max(maxWidth, width);
276
+ maxHeight = Math.max(maxHeight, height);
277
+ });
278
+
279
+ const minSpacing = 200;
280
+ const spreadFactor = 1.5; // How much to spread nodes apart
281
+ const areaSize = Math.sqrt(nodes.length) * (maxWidth + maxHeight + minSpacing) * spreadFactor;
282
+
283
+ const centerX = center?.x ?? 0;
284
+ const centerY = center?.y ?? 0;
285
+
286
+ // Place nodes randomly, checking for collisions
287
+ const layoutedNodes: Node[] = [];
288
+ const maxAttempts = 100;
289
+
290
+ nodes.forEach((node) => {
291
+ const { width, height } = getNodeDimensions(node);
292
+ let placed = false;
293
+ let attempts = 0;
294
+
295
+ while (!placed && attempts < maxAttempts) {
296
+ const x = centerX + (Math.random() - 0.5) * areaSize - width / 2;
297
+ const y = centerY + (Math.random() - 0.5) * areaSize - height / 2;
298
+
299
+ // Check if this position collides with existing nodes
300
+ const collides = layoutedNodes.some((existingNode) => {
301
+ const exDims = getNodeDimensions(existingNode);
302
+ const dx = Math.abs(x - existingNode.position.x);
303
+ const dy = Math.abs(y - existingNode.position.y);
304
+ return dx < (width + exDims.width) / 2 + minSpacing &&
305
+ dy < (height + exDims.height) / 2 + minSpacing;
306
+ });
307
+
308
+ if (!collides) {
309
+ layoutedNodes.push({
310
+ ...node,
311
+ position: { x, y },
312
+ });
313
+ placed = true;
314
+ }
315
+ attempts++;
316
+ }
317
+
318
+ // If we couldn't place it without collision, just place it anyway
319
+ if (!placed) {
320
+ layoutedNodes.push({
321
+ ...node,
322
+ position: {
323
+ x: centerX + (Math.random() - 0.5) * areaSize - width / 2,
324
+ y: centerY + (Math.random() - 0.5) * areaSize - height / 2,
325
+ },
326
+ });
327
+ }
328
+ });
120
329
 
121
330
  return {
122
331
  nodes: layoutedNodes,
123
332
  edges,
124
- width,
125
- height,
333
+ width: areaSize,
334
+ height: areaSize,
126
335
  };
127
336
  }
128
337
 
@@ -135,12 +344,14 @@ export function applyDagreLayout(
135
344
  edges: Edge[],
136
345
  direction: 'TB' | 'LR' = 'TB',
137
346
  nodeSpacing?: number,
138
- rankSpacing?: number
347
+ rankSpacing?: number,
348
+ center?: { x: number; y: number }
139
349
  ): Node[] {
140
350
  const result = applyHierarchicalLayout(nodes, edges, {
141
351
  direction,
142
352
  nodeSpacing,
143
- rankSpacing
353
+ rankSpacing,
354
+ center
144
355
  });
145
356
  return result.nodes;
146
357
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flock-core
3
- Version: 0.5.0b56
3
+ Version: 0.5.0b58
4
4
  Summary: Add your description here
5
5
  Author-email: Andre Ratzenberger <andre.ratzenberger@whiteduck.de>
6
6
  License-File: LICENSE
@@ -62,7 +62,8 @@ prompt = """You are an expert code reviewer. When you receive code, you should..
62
62
 
63
63
  # 500-line prompt that breaks when models update
64
64
 
65
- # How do I know that there isn't an even better prompt (you don't) -> proof of 'best possible performane' impossible
65
+ # How do I know that there isn't an even better prompt (you don't)
66
+ # -> proof of 'best possible performane' impossible
66
67
  ```
67
68
 
68
69
  **🧪 Testing Nightmares**
@@ -88,7 +89,8 @@ workflow.add_edge("agent_b", "agent_c")
88
89
  **🧠 God object anti-pattern:**
89
90
  ```python
90
91
  # One orchestrator needs domain knowledge of 20+ agents to route correctly
91
- # Orchestrator 'guesses' next agent based on a natural language description. Hardly fit for critical systems.
92
+ # Orchestrator 'guesses' next agent based on a natural language description.
93
+ # Hardly fit for critical systems.
92
94
  ```
93
95
 
94
96
  These aren't framework limitations, they're **architectural choices** that don't scale.
@@ -105,8 +107,51 @@ Flock takes a different path, combining two proven patterns:
105
107
 
106
108
  **Traditional approach:**
107
109
  ```python
108
- prompt = "Analyze this bug report and return JSON with severity, category, hypothesis..."
109
- result = llm.invoke(prompt) # Hope it works
110
+ prompt = """You are an expert software engineer and bug analyst. Your task is to analyze bug reports and provide structured diagnostic information.
111
+
112
+ INSTRUCTIONS:
113
+ 1. Read the bug report carefully
114
+ 2. Determine the severity level (must be exactly one of: Critical, High, Medium, Low)
115
+ 3. Classify the bug category (e.g., "performance", "security", "UI", "data corruption")
116
+ 4. Formulate a root cause hypothesis (minimum 50 characters)
117
+ 5. Assign a confidence score between 0.0 and 1.0
118
+
119
+ OUTPUT FORMAT:
120
+ You MUST return valid JSON with this exact structure:
121
+ {
122
+ "severity": "string (Critical|High|Medium|Low)",
123
+ "category": "string",
124
+ "root_cause_hypothesis": "string (minimum 50 characters)",
125
+ "confidence_score": "number (0.0 to 1.0)"
126
+ }
127
+
128
+ VALIDATION RULES:
129
+ - severity: Must be exactly one of: Critical, High, Medium, Low (case-sensitive)
130
+ - category: Must be a single word or short phrase describing the bug type
131
+ - root_cause_hypothesis: Must be at least 50 characters long and explain the likely cause
132
+ - confidence_score: Must be a decimal number between 0.0 and 1.0 inclusive
133
+
134
+ EXAMPLES:
135
+ Input: "App crashes when user clicks submit button"
136
+ Output: {"severity": "Critical", "category": "crash", "root_cause_hypothesis": "Null pointer exception in form validation logic when required fields are empty", "confidence_score": 0.85}
137
+
138
+ Input: "Login button has wrong color"
139
+ Output: {"severity": "Low", "category": "UI", "root_cause_hypothesis": "CSS class override not applied correctly in the theme configuration", "confidence_score": 0.9}
140
+
141
+ IMPORTANT:
142
+ - Do NOT include any explanatory text before or after the JSON
143
+ - Do NOT use markdown code blocks (no ```json```)
144
+ - Do NOT add comments in the JSON
145
+ - Ensure the JSON is valid and parseable
146
+ - If you cannot determine something, use your best judgment
147
+ - Never return null values
148
+
149
+ Now analyze this bug report:
150
+ {bug_report_text}"""
151
+
152
+ result = llm.invoke(prompt) # 500-line prompt that breaks when models update
153
+ # Then parse and hope it's valid JSON
154
+ data = json.loads(result.content) # Crashes in production 🔥
110
155
  ```
111
156
 
112
157
  **The Flock way:**
@@ -405,7 +450,11 @@ The dashboard provides comprehensive real-time visibility into your agent system
405
450
  - **Interactive Graph:**
406
451
  - Drag nodes, zoom, pan, and explore topology
407
452
  - Double-click nodes to open detail windows
408
- - Right-click for context menu and modules
453
+ - Right-click for context menu with auto-layout options:
454
+ - **5 Layout Algorithms**: Hierarchical (Vertical/Horizontal), Circular, Grid, and Random
455
+ - **Smart Spacing**: Dynamic 200px minimum clearance based on node dimensions
456
+ - **Viewport Centering**: Layouts always center around current viewport
457
+ - Add modules dynamically from context menu
409
458
 
410
459
  - **Advanced Filtering:**
411
460
  - Correlation ID tracking for workflow tracing
@@ -18,13 +18,13 @@ flock/dashboard/collector.py,sha256=dF8uddDMpOSdxGkhDSAvRNNaABo-TfOceipf1SQmLSU,
18
18
  flock/dashboard/events.py,sha256=ujdmRJK-GQubrv43qfQ73dnrTj7g39VzBkWfmskJ0j8,5234
19
19
  flock/dashboard/launcher.py,sha256=zXWVpyLNxCIu6fJ2L2j2sJ4oDWTvkxhT4FWz7K6eooM,8122
20
20
  flock/dashboard/service.py,sha256=5QGPT2xSbMx6Zd9_GSKJ8QtxOnBucx9BZAQhpWyUfgw,32097
21
- flock/dashboard/websocket.py,sha256=gGJPNLy4OR-0eTKJQ3oFmZVjCq-ulMNrKJVFCFcprhU,9003
21
+ flock/dashboard/websocket.py,sha256=RdJ7fhjNYJR8WHJ19wWdf9GEQtuKE14NmUpqm-QsLnA,9013
22
22
  flock/engines/__init__.py,sha256=waNyObJ8PKCLFZL3WUFynxSK-V47m559P3Px-vl_OSc,124
23
23
  flock/engines/dspy_engine.py,sha256=Q2gPYLW_f8f-JuBYOMtjtoCZH8Fc447zWF8cHpJl4ew,34538
24
24
  flock/frontend/README.md,sha256=OFdOItV8FGifmUDb694rV2xLC0vl1HlR5KBEtYv5AB0,25054
25
25
  flock/frontend/index.html,sha256=BFg1VR_YVAJ_MGN16xa7sT6wTGwtFYUhfJhGuKv89VM,312
26
- flock/frontend/package-lock.json,sha256=5c-ZQHqHYZ2CHlsyHOBss-tncxplSuHO3mddWt_-jJM,150998
27
- flock/frontend/package.json,sha256=MXt8jZodw6cysyJF-Hb50L_5AoMCEma5F80YGCp8fC4,1258
26
+ flock/frontend/package-lock.json,sha256=F-KmPhq6IbHXzxtmHL0S7dt6DGs4fWrOkrfKeXaSx6U,150998
27
+ flock/frontend/package.json,sha256=X5mMewghkW5K03Y4CI6unF2GIGbV0QoS-kZ04RNtQ3k,1258
28
28
  flock/frontend/tsconfig.json,sha256=B9p9jXohg_jrCZAq5_yIHvznpeXHiHQkwUZrVE2oMRA,705
29
29
  flock/frontend/tsconfig.node.json,sha256=u5_YWSqeNkZBRBIZ8Q2E2q6bospcyF23mO-taRO7glc,233
30
30
  flock/frontend/vite.config.ts,sha256=OQOr6Hl75iW7EvEm2_GXFdicYWthgdLhp4lz3d7RkJA,566
@@ -71,7 +71,7 @@ flock/frontend/src/components/filters/TimeRangeFilter.test.tsx,sha256=4vkGYNecXc
71
71
  flock/frontend/src/components/filters/TimeRangeFilter.tsx,sha256=xwTPRl-NbSEvSrX4krvLPb40GvCjdoUp-FqRbVZBzbU,3148
72
72
  flock/frontend/src/components/graph/AgentNode.test.tsx,sha256=Wd3AGe1E9GJ26wyRU1AUakki9AQ9euYsKx5lhi1xAfg,1966
73
73
  flock/frontend/src/components/graph/AgentNode.tsx,sha256=JOSMiR_YXq6MgME2DfhqHldS_4zZEPySEIEM6t4fg48,12447
74
- flock/frontend/src/components/graph/GraphCanvas.tsx,sha256=KDnxl3QQNLeIshgIXHyH0A73VOoyAhbboBax09cLjtI,13747
74
+ flock/frontend/src/components/graph/GraphCanvas.tsx,sha256=vMLbZie9Isnlr2pflQTRGLgME8a8Q8zXRg4qLGW7Vw4,20809
75
75
  flock/frontend/src/components/graph/MessageFlowEdge.tsx,sha256=OWYmmlS5P9yIxfCddEleEd27MA-djMd_3fcQmyT22r4,3914
76
76
  flock/frontend/src/components/graph/MessageNode.test.tsx,sha256=S0vmtk2r4HGm8844Pb65syim-ZsCqvia24CECGgq5SY,1828
77
77
  flock/frontend/src/components/graph/MessageNode.tsx,sha256=axwYof1z3UtLO0QDPfTKnZX6w_TNFlZvVJsun1aDD_k,3645
@@ -108,7 +108,7 @@ flock/frontend/src/services/api.ts,sha256=hGkImiimpSOX9xjmX5flI1Ug6YVh7EbRr7l0rq
108
108
  flock/frontend/src/services/indexeddb.test.ts,sha256=lY1JzyKRd4Kaw135JlO7njy5rpaAsRRXLM8SwYxDhAc,27452
109
109
  flock/frontend/src/services/indexeddb.ts,sha256=Eadllc6tPh09RhJPjCd6MO0tdh8PWX5Hoxy8HX9HQS0,26915
110
110
  flock/frontend/src/services/layout.test.ts,sha256=-KwUxWCum_Rsyc5NIpk99UB3prfAkMO5ksJULhjOiwA,16174
111
- flock/frontend/src/services/layout.ts,sha256=hfGZdR-qFnrb1sSVw63mV4XGSgvkHo0ONjsyptflaa0,3665
111
+ flock/frontend/src/services/layout.ts,sha256=5WzlOv7OBlQXiUxrv4l1JwaAfHbUK1C99JOT0fQCNRY,9503
112
112
  flock/frontend/src/services/themeApplicator.ts,sha256=utRFw-45e1IEOrI6lHkB_E_-5kc2kFKbN-veAUdXiOM,5802
113
113
  flock/frontend/src/services/themeService.ts,sha256=ltDAE30KzzVFDQJGm8awN0Go-l16NgTWYOxlvgxvx0E,1743
114
114
  flock/frontend/src/services/websocket.test.ts,sha256=Ix5dQI3lzKJPyFPcTs5yXxu8ZZoJ05f1CWgcQEpCOtg,17062
@@ -508,8 +508,8 @@ flock/themes/zenburned.toml,sha256=UEmquBbcAO3Zj652XKUwCsNoC2iQSlIh-q5c6DH-7Kc,1
508
508
  flock/themes/zenwritten-dark.toml,sha256=-dgaUfg1iCr5Dv4UEeHv_cN4GrPUCWAiHSxWK20X1kI,1663
509
509
  flock/themes/zenwritten-light.toml,sha256=G1iEheCPfBNsMTGaVpEVpDzYBHA_T-MV27rolUYolmE,1666
510
510
  flock/utility/output_utility_component.py,sha256=yVHhlIIIoYKziI5UyT_zvQb4G-NsxCTgLwA1wXXTTj4,9047
511
- flock_core-0.5.0b56.dist-info/METADATA,sha256=BU-uf938Ij4swNJlk6MSpyTrgisSL4fECmkH6wDQgd8,32490
512
- flock_core-0.5.0b56.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
513
- flock_core-0.5.0b56.dist-info/entry_points.txt,sha256=UQdPmtHd97gSA_IdLt9MOd-1rrf_WO-qsQeIiHWVrp4,42
514
- flock_core-0.5.0b56.dist-info/licenses/LICENSE,sha256=U3IZuTbC0yLj7huwJdldLBipSOHF4cPf6cUOodFiaBE,1072
515
- flock_core-0.5.0b56.dist-info/RECORD,,
511
+ flock_core-0.5.0b58.dist-info/METADATA,sha256=1frs-b0VPrAk3XwsqCa6pG03VeE0cDID7WtFsMeeezQ,34719
512
+ flock_core-0.5.0b58.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
513
+ flock_core-0.5.0b58.dist-info/entry_points.txt,sha256=UQdPmtHd97gSA_IdLt9MOd-1rrf_WO-qsQeIiHWVrp4,42
514
+ flock_core-0.5.0b58.dist-info/licenses/LICENSE,sha256=U3IZuTbC0yLj7huwJdldLBipSOHF4cPf6cUOodFiaBE,1072
515
+ flock_core-0.5.0b58.dist-info/RECORD,,