diff --git a/src/components/__tests__/GenerationComposer.test.tsx b/src/components/__tests__/GenerationComposer.test.tsx
index b28a1bf7..6941e0ac 100644
--- a/src/components/__tests__/GenerationComposer.test.tsx
+++ b/src/components/__tests__/GenerationComposer.test.tsx
@@ -427,6 +427,36 @@ describe("GenerationComposer", () => {
expect(screen.getByPlaceholderText(/Describe what you want to generate/)).toHaveClass("h-[13.5rem]");
});
+ it("positions the selected node editor below grouped nodes using absolute canvas coordinates", () => {
+ useWorkflowStore.setState({
+ nodes: [
+ {
+ id: "group-node-group-1",
+ type: "group",
+ position: { x: 400, y: 200 },
+ data: { groupId: "group-1" },
+ style: { width: 500, height: 360 },
+ } as WorkflowNode,
+ {
+ ...imageNode("img-1", true),
+ parentId: "group-node-group-1",
+ extent: "parent",
+ expandParent: true,
+ groupId: "group-1",
+ position: { x: 100, y: 40 },
+ measured: { width: 260, height: 180 },
+ } as WorkflowNode,
+ ],
+ });
+
+ render();
+
+ expect(screen.getByTestId("node-inline-composer")).toHaveStyle({
+ left: "108px",
+ top: "436px",
+ });
+ });
+
it("keeps the selected node editor below the node when below would leave the viewport", () => {
const originalInnerHeight = window.innerHeight;
const originalInnerWidth = window.innerWidth;
diff --git a/src/components/composer/GenerationComposer.tsx b/src/components/composer/GenerationComposer.tsx
index 4194674b..03d92957 100644
--- a/src/components/composer/GenerationComposer.tsx
+++ b/src/components/composer/GenerationComposer.tsx
@@ -892,6 +892,30 @@ function getAdapter(nodeType: NodeType | null): ComposerAdapter [item.id, item]));
+ const visited = new Set();
+
+ const resolve = (current: WorkflowNode): { x: number; y: number } => {
+ if (!current.parentId || visited.has(current.id)) return current.position;
+ visited.add(current.id);
+
+ const parent = nodeById.get(current.parentId);
+ if (!parent) return current.position;
+
+ const parentPosition = resolve(parent);
+ return {
+ x: parentPosition.x + current.position.x,
+ y: parentPosition.y + current.position.y,
+ };
+ };
+
+ return resolve(node);
+}
+
function deriveComposerContext(nodes: WorkflowNode[], edges: WorkflowEdge[]): ComposerContext {
const selectedNodes = nodes.filter((node) => node.selected);
if (selectedNodes.length === 0) {
@@ -1702,6 +1726,10 @@ export function GenerationComposer() {
() => context.node ? getComposerNodeDimensions(context.node) : null,
[context.node]
);
+ const selectedNodePosition = useMemo(
+ () => context.node ? getAbsoluteNodePosition(context.node, nodes) : null,
+ [context.node, nodes]
+ );
const safeZoom = zoom > 0 ? zoom : 1;
const viewportWidth = typeof window === "undefined" ? Number.POSITIVE_INFINITY : window.innerWidth;
const visibleLeft = -viewportX / safeZoom;
@@ -1709,15 +1737,15 @@ export function GenerationComposer() {
const composerWidth = isComposerCollapsed ? COMPOSER_COLLAPSED_WIDTH : COMPOSER_EXPANDED_WIDTH;
const composerFlowWidth = composerWidth / safeZoom;
const viewportMargin = COMPOSER_VIEWPORT_MARGIN / safeZoom;
- const inlineComposerPosition = context.node && selectedNodeDimensions
+ const inlineComposerPosition = context.node && selectedNodeDimensions && selectedNodePosition
? (() => {
- const centeredLeft = context.node.position.x + selectedNodeDimensions.width / 2 - composerFlowWidth / 2;
+ const centeredLeft = selectedNodePosition.x + selectedNodeDimensions.width / 2 - composerFlowWidth / 2;
const minLeft = visibleLeft + viewportMargin;
const maxLeft = visibleRight - composerFlowWidth - viewportMargin;
const left = maxLeft >= minLeft
? Math.min(Math.max(centeredLeft, minLeft), maxLeft)
: minLeft;
- const belowTop = context.node.position.y + selectedNodeDimensions.height + viewportMargin;
+ const belowTop = selectedNodePosition.y + selectedNodeDimensions.height + viewportMargin;
return {
left,