From 36cf4f39e1a0b82b2eec4eb4fe3981d6b1f19437 Mon Sep 17 00:00:00 2001 From: "Dr. Dominik Jain" Date: Mon, 31 Aug 2026 14:10:28 +0200 Subject: [PATCH] :sparkles: Apply method Penpot.waitForLayoutUpdate, removing workarounds (#11433) * MCP server: Update instruction manual to point to method instead of sleep * MCP plugin: Call waitForLayoutUpdate prior to every code execution * Composable test suite: Use waitForLayoutUpdate instead of 200ms delay in tests --- .../src/task-handlers/ExecuteCodeTaskHandler.ts | 11 +++++++++++ mcp/packages/server/data/initial_instructions.md | 12 ++++++++++-- .../src/composable-tests/operations/OpAssert.ts | 9 ++------- .../src/composable-tests/util/PenpotSync.ts | 9 ++++++++- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts b/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts index df2fb65832..87ea2c7616 100644 --- a/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts +++ b/mcp/packages/plugin/src/task-handlers/ExecuteCodeTaskHandler.ts @@ -214,6 +214,17 @@ export class ExecuteCodeTaskHandler extends TaskHandler { let result: any; try { + // wait for layout updates prior to executing the supplied code (if method is available) + try { + // @ts-ignore - TODO Penpot.waitForLayoutUpdate is not yet in the released types + if (penpot.waitForLayoutUpdate) { + // @ts-ignore + await penpot.waitForLayoutUpdate(); + } + } catch (e) { + console.error("Error waiting for layout update:", e); + } + // execute the code in an async function with the context variables as parameters result = await (async (ctx) => { const fn = new Function(...Object.keys(ctx), `return (async () => { ${code} })();`); diff --git a/mcp/packages/server/data/initial_instructions.md b/mcp/packages/server/data/initial_instructions.md index 34aaf95d16..be9177619c 100644 --- a/mcp/packages/server/data/initial_instructions.md +++ b/mcp/packages/server/data/initial_instructions.md @@ -16,6 +16,14 @@ This is the full list of types/interfaces in the Penpot API: $api_types You use the `storage` object extensively to store data and utility functions you define across tool calls. This allows you to inspect intermediate results while still being able to build on them in subsequent code executions. +## Asynchronous Updates + +Changes made to a design may take effect asynchronously. +So if you need to read the result of your executions/observe properties affected by a change in the same `execude_code` call, use +`await penpot.waitForLayoutUpdate();` +before trying to observe changes. +Every `execude_code` call waits for updates before applying your code, so you never need to call `waitForLayoutUpdate` at the beginning of your code. + # The Structure of Penpot Designs A Penpot design ultimately consists of shapes. @@ -144,7 +152,7 @@ Boards can have layout systems that automatically control the positioning and sp it only changes the formal bounding box; if the text does not fit it, it will overflow; use `textBounds` for the actual bounding box of the rendered text. * Property `bounds` is sized automatically (in one dimension) if the `growType` property is set to "auto-width" or "auto-height". `resize` always sets `growType` to "fixed", so ALWAYS set it back to "auto-width" or "auto-height" if you want automatic sizing! - The auto-sizing is not immediate; sleep for a short time (100ms) if you want to read the updated bounding box. + The auto-sizing is asynchronous; use `waitForLayoutUpdate` before reading the updated bounding box. * Method `getRange(start, end): TextRange` to reference a range of characters as a `TextRange` object, which can be styled separately from the rest of the text; `start` index inclusive, `end` exclusive * Other Writable font properties: `fontId`, `fontFamily`, `fontWeight`, `fontVariant`, `fontStyle` - To discover valid values, check available fonts in `penpot.fonts: FontContext` @@ -391,7 +399,7 @@ Applying tokens: - TokenTextDecorationProps: "textDecoration" - TokenTypographyProps: "typography" * `token.applyToShapes(shapes, properties)` - Apply from token - * Application is **asynchronous** (wait for ~100ms to see the effects) + * Application is **asynchronous** (use `waitForLayoutUpdate`) * After application: - `shape.tokens` returns a mapping `{ propertyName: "token.name" }` from `TokenProperty` to token name - The actual shape properties that the tokens control will reflect the token's resolved value. diff --git a/plugins/apps/composable-test-suite/src/composable-tests/operations/OpAssert.ts b/plugins/apps/composable-test-suite/src/composable-tests/operations/OpAssert.ts index 3158075b9d..67951c0683 100644 --- a/plugins/apps/composable-test-suite/src/composable-tests/operations/OpAssert.ts +++ b/plugins/apps/composable-test-suite/src/composable-tests/operations/OpAssert.ts @@ -27,13 +27,8 @@ export class OpAssert extends Operation { } async applyTo(situation: Situation): Promise { - try { - this.assertion(situation); - } catch { - // a read may have raced propagation; let it settle and check once more - await PenpotSync.awaitPropagation(); - this.assertion(situation); - } + await PenpotSync.awaitPropagation(); + this.assertion(situation); } toString(): string { diff --git a/plugins/apps/composable-test-suite/src/composable-tests/util/PenpotSync.ts b/plugins/apps/composable-test-suite/src/composable-tests/util/PenpotSync.ts index a2334ce832..ff4db17676 100644 --- a/plugins/apps/composable-test-suite/src/composable-tests/util/PenpotSync.ts +++ b/plugins/apps/composable-test-suite/src/composable-tests/util/PenpotSync.ts @@ -12,6 +12,13 @@ export class PenpotSync { * explicit "wait for propagation" primitive. */ static awaitPropagation(): Promise { - return new Promise((resolve) => setTimeout(resolve, PenpotSync.PROPAGATION_MS)); + // @ts-ignore + if (penpot.waitForLayoutUpdate) { + // @ts-ignore + return penpot.waitForLayoutUpdate(); + } else { + throw new Error("PenpotSync.awaitPropagation: waitForLayoutUpdate is not available"); + return new Promise((resolve) => setTimeout(resolve, PenpotSync.PROPAGATION_MS)); + } } }