Skip to content

Commit 782a110

Browse files
committed
fix(workbench): stagger engine task placement
1 parent 539ada3 commit 782a110

File tree

2 files changed

+114
-6
lines changed

2 files changed

+114
-6
lines changed

apps/workbench/src/features/swarm/hooks/__tests__/use-engine-board-bridge.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,57 @@ describe("useEngineBoardBridge", () => {
428428
}
429429
});
430430

431+
it("stacks runtime-created tasks under the same agent instead of overlapping them", () => {
432+
const events = new TypedEventEmitter<SwarmEngineEventMap>();
433+
const engine = {
434+
getState: () => makeEngineState(),
435+
getEvents: () => events,
436+
};
437+
438+
const { unmount } = renderHook(() =>
439+
useEngineBoardBridge(engine as any),
440+
);
441+
442+
try {
443+
act(() => {
444+
events.emit("task.created", {
445+
task: {
446+
id: "tsk_1",
447+
name: "Queue scan",
448+
type: "analysis",
449+
status: "created",
450+
assignedTo: "agt_pool_1",
451+
},
452+
} as any);
453+
454+
events.emit("task.created", {
455+
task: {
456+
id: "tsk_2",
457+
name: "Trace graph",
458+
type: "analysis",
459+
status: "created",
460+
assignedTo: "agt_pool_1",
461+
},
462+
} as any);
463+
});
464+
465+
const taskPositions = useSwarmBoardStore.getState().nodes
466+
.filter((node) => node.data.taskId === "tsk_1" || node.data.taskId === "tsk_2")
467+
.sort((left, right) => left.id.localeCompare(right.id))
468+
.map((node) => ({
469+
id: node.id,
470+
position: node.position,
471+
}));
472+
473+
expect(taskPositions).toEqual([
474+
{ id: "tsk_1", position: { x: 240, y: 320 } },
475+
{ id: "tsk_2", position: { x: 240, y: 344 } },
476+
]);
477+
} finally {
478+
unmount();
479+
}
480+
});
481+
431482
it("maps task statuses safely and binds tasks when assignment happens after creation", () => {
432483
const events = new TypedEventEmitter<SwarmEngineEventMap>();
433484
const engine = {

apps/workbench/src/features/swarm/hooks/use-engine-board-bridge.ts

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import { nextNodePosition } from "./node-position";
1717

1818
const EVAL_GLOW_DURATION_MS = 2000;
1919
const DEFAULT_LAYOUT_VIEWPORT = { width: 1200, height: 800 };
20+
const TASK_VERTICAL_OFFSET = 200;
21+
const TASK_VERTICAL_STAGGER = 24;
2022

2123
function mapEngineStatus(engineStatus: string): SessionStatus {
2224
switch (engineStatus) {
@@ -96,10 +98,20 @@ function seedBoardFromEngineSnapshot(engine: SwarmOrchestrator): void {
9698
position: agentPositions.get(agent.id),
9799
}));
98100

99-
const taskNodes = Object.values(snapshot.tasks).map((task, index) => {
101+
const seededTaskCounts = new Map<string, number>();
102+
103+
const taskNodes = Object.values(snapshot.tasks).map((task) => {
100104
const agentPosition = task.assignedTo
101105
? agentPositions.get(task.assignedTo)
102106
: undefined;
107+
const taskIndex = task.assignedTo
108+
? (seededTaskCounts.get(task.assignedTo) ?? 0)
109+
: 0;
110+
111+
if (task.assignedTo) {
112+
seededTaskCounts.set(task.assignedTo, taskIndex + 1);
113+
}
114+
103115
return {
104116
id: task.id,
105117
taskId: task.id,
@@ -115,7 +127,7 @@ function seedBoardFromEngineSnapshot(engine: SwarmOrchestrator): void {
115127
previewLines: task.previewLines,
116128
},
117129
position: agentPosition
118-
? { x: agentPosition.x, y: agentPosition.y + 200 + index * 24 }
130+
? getStackedTaskPosition(agentPosition, taskIndex)
119131
: undefined,
120132
};
121133
});
@@ -177,6 +189,47 @@ function buildTopologyEdges(
177189
});
178190
}
179191

192+
function getStackedTaskPosition(
193+
agentPosition: { x: number; y: number },
194+
taskIndex: number,
195+
): { x: number; y: number } {
196+
return {
197+
x: agentPosition.x,
198+
y: agentPosition.y + TASK_VERTICAL_OFFSET + taskIndex * TASK_VERTICAL_STAGGER,
199+
};
200+
}
201+
202+
function getAssignedTaskPosition(
203+
nodes: Node<SwarmBoardNodeData>[],
204+
agentId: string | null | undefined,
205+
excludeTaskNodeId?: string,
206+
): { x: number; y: number } | undefined {
207+
if (!agentId) {
208+
return undefined;
209+
}
210+
211+
const agentNode = nodes.find(
212+
(node) => node.data.agentId === agentId,
213+
);
214+
215+
if (!agentNode) {
216+
return undefined;
217+
}
218+
219+
const siblingTaskCount = nodes.filter((node) => {
220+
if (node.id === excludeTaskNodeId) {
221+
return false;
222+
}
223+
224+
return (
225+
node.data.nodeType === "terminalTask" &&
226+
node.data.agentId === agentId
227+
);
228+
}).length;
229+
230+
return getStackedTaskPosition(agentNode.position, siblingTaskCount);
231+
}
232+
180233
function getLayoutViewport(): { width: number; height: number } {
181234
if (typeof window === "undefined") {
182235
return DEFAULT_LAYOUT_VIEWPORT;
@@ -292,9 +345,10 @@ export function useEngineBoardBridge(engine: SwarmOrchestrator | null): void {
292345
n.data.agentId === event.task.assignedTo,
293346
);
294347

295-
const position = parentNode
296-
? { x: parentNode.position.x, y: parentNode.position.y + 200 }
297-
: nextNodePosition(nodes);
348+
const position = getAssignedTaskPosition(
349+
nodes,
350+
event.task.assignedTo,
351+
) ?? nextNodePosition(nodes);
298352

299353
const taskNode = {
300354
...createBoardNode({
@@ -363,7 +417,10 @@ export function useEngineBoardBridge(engine: SwarmOrchestrator | null): void {
363417
return {
364418
...node,
365419
position: agentNode
366-
? { x: agentNode.position.x, y: agentNode.position.y + 200 }
420+
? (
421+
getAssignedTaskPosition(nodes, event.agentId, taskNode.id) ??
422+
node.position
423+
)
367424
: node.position,
368425
data: {
369426
...node.data,

0 commit comments

Comments
 (0)