Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, unknown>
shiftSourceBlockIds,
horizontalSpacing: DEFAULT_HORIZONTAL_SPACING,
verticalSpacing: DEFAULT_VERTICAL_SPACING,
previousBlocks: workflowState.blocks,
})
} catch (error) {
logger.warn('Targeted autolayout failed, using default positions', {
Expand Down
61 changes: 61 additions & 0 deletions apps/sim/lib/workflows/autolayout/targeted.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,4 +458,65 @@ describe('applyTargetedLayout', () => {
result.loop.position.x + loopMetrics.width
)
})

it('relocates a newly added note off a block it was created on top of', () => {
// A copilot note is born at the (0,0) placeholder, and a fresh workflow's
// start block lives at (0,0) too. The pre-edit snapshot does not contain
// the note, so the overlap counts as introduced by this edit and the note
// must be moved to the stack below the flow, not preserved as intentional.
const blocks = {
start: createBlock('start', { position: { x: 0, y: 0 } }),
note: createBlock('note', {
type: 'note',
position: { x: 0, y: 0 },
subBlocks: {
content: { id: 'content', type: 'long-input', value: 'Explains the workflow' },
},
}),
}

const result = applyTargetedLayout(blocks, [], {
changedBlockIds: ['note'],
previousBlocks: { start: blocks.start },
})

const startMetrics = getBlockMetrics(result.start)
expect(result.start.position).toEqual({ x: 0, y: 0 })
expect(result.note.position.y).toBeGreaterThanOrEqual(
result.start.position.y + startMetrics.height + DEFAULT_VERTICAL_SPACING
)
})

it('preserves a pre-existing note arrangement when an unrelated block is laid out', () => {
const blocks = {
start: createBlock('start', { position: { x: 0, y: 0 } }),
note: createBlock('note', {
type: 'note',
position: { x: 60, y: 10 },
subBlocks: {
content: { id: 'content', type: 'long-input', value: 'Deliberately parked here' },
},
}),
added: createBlock('added', { position: { x: 0, y: 0 } }),
}

const edges: Edge[] = [{ id: 'e1', source: 'start', target: 'added' }]

const result = applyTargetedLayout(blocks, edges, {
changedBlockIds: ['added'],
previousBlocks: {
start: createBlock('start', { position: { x: 0, y: 0 } }),
note: createBlock('note', {
type: 'note',
position: { x: 60, y: 10 },
subBlocks: {
content: { id: 'content', type: 'long-input', value: 'Deliberately parked here' },
},
}),
},
})

expect(result.note.position).toEqual({ x: 60, y: 10 })
expect(result.added.position.x).toBeGreaterThan(result.start.position.x)
})
})
16 changes: 14 additions & 2 deletions apps/sim/lib/workflows/autolayout/targeted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ export interface TargetedLayoutOptions extends LayoutOptions {
shiftSourceBlockIds?: string[]
verticalSpacing?: number
horizontalSpacing?: number
/**
* Pre-edit block snapshot used to judge whether a note's overlap with a block
* is a pre-existing arrangement (preserved) or was introduced by the edit
* being laid out (relocated). Without it, the post-edit input blocks serve as
* the baseline — which contains newly added notes at their `(0,0)` placeholder,
* so a new note dropped onto a block at the origin (e.g. a fresh workflow's
* start block) reads as intentional and is never rescued.
*/
previousBlocks?: Record<string, BlockState>
}

/**
Expand All @@ -53,6 +62,7 @@ export function applyTargetedLayout(
verticalSpacing = DEFAULT_VERTICAL_SPACING,
horizontalSpacing = DEFAULT_HORIZONTAL_SPACING,
gridSize,
previousBlocks = blocks,
} = options

if (
Expand Down Expand Up @@ -135,8 +145,10 @@ export function applyTargetedLayout(
)

// Relocate notes only where this pass introduced an overlap, comparing against
// the original positions so pre-existing note arrangements are preserved.
resolveNoteOverlaps(blocksCopy, verticalSpacing, { previousBlocks: blocks })
// the baseline positions so pre-existing note arrangements are preserved. A
// note absent from the baseline (newly added by this edit) is always eligible
// for relocation.
resolveNoteOverlaps(blocksCopy, verticalSpacing, { previousBlocks })

return blocksCopy
}
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/workflows/diff/diff-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ export class WorkflowDiffEngine {
shiftSourceBlockIds,
horizontalSpacing: DEFAULT_HORIZONTAL_SPACING,
verticalSpacing: DEFAULT_VERTICAL_SPACING,
previousBlocks: mergedBaseline.blocks,
})

Object.entries(layoutedBlocks).forEach(([id, layoutBlock]) => {
Expand Down
Loading