* ✨ Expand interaction helper test coverage
Add coverage for interaction destination and flow helpers,
including nil handling and removal helpers. Document the
intent of the new assertions so future interaction changes
keep the helper contract explicit.
* ✨ Cover interaction validation edge cases
Exercise the remaining interaction guards and overlay
positioning edge cases, including invalid state
transitions and nested manual offsets. Keep the test
comments focused on why each branch matters for editor
behavior.
Clamp the frame index to the valid range in zoom-to-fit and
zoom-to-fill events before accessing the frames vector. When the
URL query parameter :index exceeds the number of frames on the
page (e.g. index=1 with a single frame), nth would throw
"No item 1 in vector of length 1". Also adds unit tests covering
the boundary condition.
Extend the telemetry payload with a sorted list of unique email domains
extracted from all registered profile email addresses. The new
:email-domains field is populated via a single SQL query using
split_part and DISTINCT, and is included in the stats sent when
telemetry is enabled.
Also update the tasks-telemetry-test to assert the new field is present
and contains the expected domain values.
Return nil from get-prev-sibling when the shape is no longer present in
the parent ordering so delete undo generation falls back to index-based
restore instead of crashing on invalid vector access.
* 🐛 Handle plugin errors gracefully without crashing the UI
Plugin errors (like 'Set is not a constructor') were propagating to the
global error handler and showing the exception page. This fix:
- Uses a WeakMap to track plugin errors (works in SES hardened environment)
- Wraps setTimeout/setInterval handlers to mark errors and re-throw them
- Frontend global handler checks isPluginError and logs to console
Plugin errors are now logged to console with 'Plugin Error' prefix but
don't crash the main application or show the exception page.
Signed-off-by: AI Agent <agent@penpot.app>
* ✨ Improved handling of plugin errors on initialization
* ✨ Fix test and linter
---------
Signed-off-by: AI Agent <agent@penpot.app>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
The options stored in options-ref is a delay (lazy value). In
on-token-key-down, it was passed raw to next-focus-index without being
dereferenced first, causing count to be called on a JS object that does
not implement ICounted.
Fix: dereference the delay in on-token-key-down (matching the existing
pattern in on-key-down), and make next-focus-index itself also handle
delays defensively. Add unit tests covering the delay case.
This is a temporary workaround for penpot/penpot-mcp#27.
It adds a wait time before exports via the export_shape tool to account
for asynchronous updates in Penpot, increasing the likelihood of exporting
the fully updated state.
The root lock file not being present causes issues, because
the sub-project dependencies are managed by it.
The lack of inclusion of pnpm-lock.yaml was the root cause of #8829.
A dependency was updated incompatibly, breaking the release.
Since npm pack has a hard exclusion rule for pnpm-lock.yaml,
we include it under a different name and restore it at runtime.
If the MCP version (as given in mcp/package.json) does not match
the Penpot version (as given by penpot.version), display a warning
message in the plugin UI.
This is important for users running the local MCP server, as it
is a common failure mode to combine the MCP server with an
incompatible Penpot version.
* ✨ Use update-when for update dashboard state
This make updates more consistent and reduces possible eventual
consistency issues in out of order events execution.
* 🐛 Detect stale JS modules at boot and force reload
When the browser serves cached JS files from a previous deployment
alongside a fresh index.html, code-split modules reference keyword
constants that do not exist in the stale shared.js, causing TypeError
crashes.
This adds a compile-time version tag (via goog-define / closure-defines)
that is baked into the JS bundle. At boot, it is compared against the
runtime version tag from index.html (which is always fresh due to
no-cache headers). If they differ, the app forces a hard page reload
before initializing, ensuring all JS modules come from the same build.
* 📎 Ensure consistent version across builds on github e2e test workflow
---------
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* 🐛 Fix content attribute sync group resolution by shape type
The :content attribute was mapped to a single sync group (:content-group)
but it is used by both path and text shapes with different synchronization
needs. This caused incorrect component synchronization when editing content
on path shapes, as they should sync under :geometry-group instead of
:content-group.
Changes:
- Make sync-attrs allow type-dependent group mapping via maps
- Add resolve-sync-group and resolve-sync-groups helper functions
- Update all sync-attr lookups to use shape type for proper resolution
- Fix touched checks to handle multiple possible sync groups
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* ✨ Make PR feedback changes
* 🔥 Remove unused function
---------
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* ♻️ Handle fetch-error gracefully with toast instead of full-page error
Network-level failures (lost connectivity, DNS failure, etc.) on RPC
calls were propagating as :internal/:fetch-error to the global error
handler, which replaced the entire UI with a full-page error screen.
Now the :internal handler distinguishes :fetch-error from other internal
errors and shows a non-intrusive toast notification instead, allowing
the user to continue working.
* ✨ Add automatic retry with backoff for idempotent RPC requests
Idempotent (GET) RPC requests are now automatically retried up to 3
times with exponential back-off (1s, 2s, 4s) when a transient error
occurs. Retryable errors include: network-level failures
(:fetch-error), 502 Bad Gateway, 503 Service Unavailable, and browser
offline (status 0).
Mutation (POST) requests are never retried to avoid unintended
side-effects. Non-transient errors (4xx client errors, auth errors,
validation errors) propagate immediately without retry.
* ♻️ Make retry helpers public with configurable parameters
Make retryable-error? and with-retry public functions, and replace
private constants with a default-retry-config map. with-retry now
accepts an optional config map (:max-retries, :base-delay-ms) enabling
callers and tests to customize retry behavior.
* ✨ Add tests for RPC retry mechanism
Comprehensive tests for the retry helpers in app.main.repo:
- retryable-error? predicate: covers all retryable types (fetch-error,
bad-gateway, service-unavailable, offline) and non-retryable types
(validation, authentication, authorization, plain errors)
- with-retry observable wrapper: verifies immediate success, recovery
after transient failures, max-retries exhaustion, no retry for
non-retryable errors, fetch-error retry, custom config, and mixed
error scenarios
* ♻️ Introduce :network error type for fetch-level failures
Replace the awkward {:type :internal :code :fetch-error} combination
with a proper {:type :network} type in app.util.http/fetch. This makes
the error taxonomy self-explanatory and removes the special-case branch
in the :internal handler.
Consequences:
- http.cljs: emit {:type :network} instead of {:type :internal :code :fetch-error}
- errors.cljs: add a dedicated ptk/handle-error :network method (toast);
restore :internal handler to its original unconditional full-page error form
- repo.cljs: simplify retryable-types and retryable-error? — :network
replaces the former :internal special-case, no code check needed
- repo_test.cljs: update tests to use {:type :network}
* 📚 Add comment explaining the use of bit-shift-left