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
This commit is contained in:
Dr. Dominik Jain 2026-08-31 14:10:28 +02:00 committed by GitHub
parent 292390f0eb
commit 36cf4f39e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 31 additions and 10 deletions

View File

@ -214,6 +214,17 @@ export class ExecuteCodeTaskHandler extends TaskHandler<ExecuteCodeTaskParams> {
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} })();`);

View File

@ -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.

View File

@ -27,13 +27,8 @@ export class OpAssert extends Operation {
}
async applyTo(situation: Situation): Promise<void> {
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 {

View File

@ -12,6 +12,13 @@ export class PenpotSync {
* explicit "wait for propagation" primitive.
*/
static awaitPropagation(): Promise<void> {
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));
}
}
}