mirror of
https://github.com/penpot/penpot.git
synced 2026-05-23 08:53:39 +00:00
✨ Add structured memories for agents
Memories use a system of progressive disclosure: Starting from a root memory, memories reference other memories using explicit references. The new system of hierarchical memories replaces AGENTS.md files. GitHub #9215 Co-authored-by: Michael Panchenko <michael.panchenko@oraios-ai.de> Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
c7a4532838
commit
63e7df5fda
40
.serena/memories/backend/auth-permissions-product-domains.md
Normal file
40
.serena/memories/backend/auth-permissions-product-domains.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Backend Auth, Permissions, and Product Domain Subtleties
|
||||
|
||||
## Auth and sessions
|
||||
|
||||
- Main auth RPC commands live in `app.rpc.commands.auth`; LDAP and OIDC provider logic live in `app.auth.ldap` and `app.auth.oidc`, with LDAP-specific RPC checks in `app.rpc.commands.ldap`.
|
||||
- Public auth endpoints must explicitly set `::rpc/auth false`; RPC auth defaults to enabled. Session cookie creation/deletion is usually attached as an RPC response transform.
|
||||
- Basic Penpot registration is token staged: prepare/register creates or verifies temporary tokens, then profile creation/session setup is reused by other auth backends. The frontend `/auth/verify-token` flow is a hub for registration confirmation, email change, and invitation tokens.
|
||||
- OIDC-compatible providers share a generic flow: redirect to provider, validate callback/request token, fetch identity data, then login an existing profile or register a new one. Known providers may have hardcoded endpoints; generic OIDC can use discovery/configured endpoints.
|
||||
- LDAP login validates credentials against the external directory, fetches identity data, then logs in or registers a matching Penpot profile. LDAP registration is not a separate Penpot signup flow.
|
||||
- Logout may return an OIDC provider redirect URI when the session claims include provider/session data and the provider has a logout URI.
|
||||
- Invitation tokens are verified through token issuers and only accepted when the token member id/email matches the authenticated profile; otherwise login proceeds without consuming the invitation.
|
||||
- HTTP/session parsing details such as cookie/header precedence, JWT session token versions, and SameSite behavior are in `mem:backend/http-storage-filedata-subtleties`.
|
||||
|
||||
## Permission model
|
||||
|
||||
- `app.rpc.permissions` provides predicate/check factories. Failed permission checks intentionally raise `:not-found` / `:object-not-found`, not an authorization-specific error, to avoid leaking object existence.
|
||||
- Team role flags are normalized as owner > admin > editor > viewer. Owner/admin imply edit; any membership row implies read.
|
||||
- File/project/comment checks are implemented in the owning command namespaces, often via helpers imported from `files`, `teams`, or `projects`; do not bypass those helpers with direct DB lookups unless preserving their not-found semantics.
|
||||
- Comment permission includes both logged-in state and the file/team comment policy. Shared viewer paths may pass `share-id`; preserve that path when changing comment queries.
|
||||
|
||||
## Teams, projects, and invitations
|
||||
|
||||
- Team/project commands mix DB changes, email, message bus notifications, media/storage cleanup, feature flags, quotas, and audit metadata. Keep mutations transactional when the existing command does so.
|
||||
- Invitation flows validate muted/bounced emails before sending and use tokenized invitation state. Accepting an invitation is tied to the invited member identity, not just possession of a token.
|
||||
- Logical deletion is used for many product objects; prefer existing logical-deletion helpers over hard deletes unless the command already performs permanent cleanup.
|
||||
- Bounced/spam-complaint emails can mute/block a profile for login/registration and email sending. Devenv MailCatcher is the normal local path for registration/email-flow testing.
|
||||
|
||||
## Comments, webhooks, and audit
|
||||
|
||||
- Comment thread queries join file/project/profile state and exclude deleted files/projects. Unread comment counts depend on `comment_thread_status.modified-at` and profile notification preferences.
|
||||
- Webhook edits are allowed for team editors/admins or the webhook creator. Webhook validation performs a synchronous HEAD request with a short timeout; validation errors are mapped through `app.loggers.webhooks`.
|
||||
- Audit events are prepared from RPC metadata, result metadata, params, request context, and selected auth identifiers. Webhook event batching can be controlled through audit/webhook metadata on commands or results.
|
||||
- Webhook and audit logging are cross-cutting side effects of product commands; when adding a command, check nearby command metadata and result metadata patterns before inventing a new event shape.
|
||||
|
||||
## Local testing notes
|
||||
|
||||
- Enable LDAP login locally with frontend flag `enable-login-with-ldap`; the devenv includes a configured test LDAP service.
|
||||
- OIDC testing requires external provider app credentials plus matching backend/frontend config.
|
||||
- Backend domain tests usually live under `backend/test/backend_tests/rpc/commands/*_test.clj` or nearby backend test namespaces. Use focused `clojure -M:dev:test --focus ...` from `backend/` when possible.
|
||||
- For auth/session or HTTP behavior, combine backend tests with the HTTP/session notes in `mem:backend/http-storage-filedata-subtleties` because RPC-level tests may not exercise cookie/header transforms.
|
||||
63
.serena/memories/backend/core.md
Normal file
63
.serena/memories/backend/core.md
Normal file
@ -0,0 +1,63 @@
|
||||
# Backend Architecture and Workflow
|
||||
|
||||
Backend: JVM Clojure; Integrant; PostgreSQL; Redis/Valkey; RPC; HTTP; storage; mail; audit/logging; workers.
|
||||
|
||||
Focused routing: RPC/DB/workers -> `mem:backend/rpc-db-worker-subtleties`; HTTP/session/storage/media/file-data -> `mem:backend/http-storage-filedata-subtleties`; auth/permissions/product domains -> `mem:backend/auth-permissions-product-domains`.
|
||||
|
||||
## Stable namespace map
|
||||
|
||||
- `app.rpc.commands.*`: RPC command implementations exposed under `/api/rpc/command/<cmd-name>`.
|
||||
- `app.rpc.permissions`: permission predicate/check helper factories.
|
||||
- `app.http.*`: HTTP routes and middleware.
|
||||
- `app.auth.*`: provider-specific authentication helpers such as LDAP/OIDC.
|
||||
- `app.loggers.*`: audit, webhook, database, and external log integrations.
|
||||
- `app.db.*` / `app.db`: next.jdbc wrapper and SQL helpers.
|
||||
- `app.tasks.*`: background task handlers.
|
||||
- `app.worker`: task execution/cron plumbing.
|
||||
- `app.main`: Integrant system map and component wiring.
|
||||
- `app.config`: `PENPOT_*` env config and feature flags.
|
||||
- `app.srepl.*`: development REPL helpers for manual backend operations.
|
||||
- `app.nitrate`, `app.rpc.commands.nitrate`, and `app.rpc.management.nitrate`: external Nitrate subscription/organization integration, gated by the `:nitrate` feature flag and shared-key HTTP calls.
|
||||
|
||||
## RPC conventions
|
||||
|
||||
RPC commands are defined with `app.util.services/defmethod` and schemas. Use `get-` prefixes for read operations. Command metadata usually includes auth, docs version, params schema, and result schema. Return plain maps/vectors or raise structured exceptions from `app.common.exceptions`.
|
||||
|
||||
Backend RPC command areas without focused memories include access tokens, binfile, demo, feedback, file snapshots, fonts, management, Nitrate, and webhooks beyond the notes in `mem:backend/auth-permissions-product-domains`; inspect nearby command tests and command metadata before changing them.
|
||||
|
||||
## DB conventions
|
||||
|
||||
`app.db` helpers accept cfg, pool, or conn in most places and convert kebab-case to snake_case:
|
||||
- `db/get`, `db/get*`, `db/query`, `db/insert!`, `db/update!`, `db/delete!`.
|
||||
- Use `db/run!` for multiple operations on one connection.
|
||||
- Use `db/tx-run!` for transactions.
|
||||
|
||||
Development DB: `postgresql://penpot:penpot@postgres/penpot`.
|
||||
Test DB: `postgresql://penpot:penpot@postgres/penpot_test`.
|
||||
Database migrations live in `backend/src/app/migrations/`; pure SQL migrations are under `backend/src/app/migrations/sql/`. SQL filenames conventionally start with a sequence and verb/table description, e.g. `0026-mod-profile-table-add-is-active-field`. Applied migrations are tracked in the `migrations` table.
|
||||
|
||||
To inspect the whole DB schema in devenv, use `pg_dump -h postgres -s > schema.sql` from inside the environment.
|
||||
|
||||
## Background tasks
|
||||
|
||||
A task handler is an Integrant component with `ig/assert-key`, `ig/expand-key`, and `ig/init-key`, returning the function run by the worker. New tasks also need wiring in `app.main`: handler config, worker registry entry, and cron entry if scheduled.
|
||||
|
||||
## REPL and fixtures
|
||||
|
||||
In devenv, backend nREPL is exposed on port 6064. `backend/scripts/nrepl` starts a REPLy client.
|
||||
|
||||
For an in-process backend REPL, stop the running backend first so port 9090 is free, then run `backend/scripts/repl`. Useful top-level helpers include `(start)`, `(stop)`, `(restart)`, `(run-tests)`, and `(repl/refresh-all)`. Many `app.srepl.main` helpers accept the global `system` var, e.g. manual email or maintenance operations.
|
||||
|
||||
Fixtures can populate local data for manual testing/perf work. From the backend REPL, run `(app.cli.fixtures/run {:preset :small})`; fixture users conventionally look like `profileN@example.com` with password `123123`. Standalone fixture aliases may exist, but check current `backend/deps.edn` before relying on old command names.
|
||||
|
||||
## Commands
|
||||
|
||||
From `backend/`:
|
||||
- Focused test: `clojure -M:dev:test --focus backend-tests.some-ns-test`.
|
||||
- Full backend test suite: `clojure -M:dev:test` or `pnpm run test`.
|
||||
- Watch/focused testing is also available through `(run-tests ...)` in the backend REPL.
|
||||
- Lint: `pnpm run lint`.
|
||||
- Format check: `pnpm run check-fmt`.
|
||||
- Format fix: `pnpm run fmt`.
|
||||
|
||||
Use JVM type hints in performance-critical paths to avoid reflection.
|
||||
31
.serena/memories/backend/http-storage-filedata-subtleties.md
Normal file
31
.serena/memories/backend/http-storage-filedata-subtleties.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Backend HTTP, Storage, Media, and File Data Subtleties
|
||||
|
||||
## Config and HTTP/session middleware
|
||||
|
||||
- `app.config/config` and `flags` are dynamic `defonce` vars populated from `PENPOT_*` env vars through the shared schema string transformer. Tests and tooling can bind them.
|
||||
- `parse-flags` automatically adds `:disable-secure-session-cookies` when `public-uri` is plain HTTP and not localhost. This changes cookie defaults without an explicit env flag.
|
||||
- The backend sets Clojure `*assert*` globally from the `:backend-asserts` feature flag. Assertion-dependent checks can therefore differ by runtime flags.
|
||||
- Request body parsing is mostly POST-oriented and supports Transit JSON plus plain JSON. Plain JSON request keys are kebab-decoded before being merged into `:params`.
|
||||
- Response formatting negotiates with `Accept` or `_fmt=json`. Transit is the default for collection/boolean bodies; JSON encoding has special pointer-map handling.
|
||||
- Auth prefers the session cookie token before the `Authorization` header. Headers may be `Token` or `Bearer`; JWTs with `kid=1` and `ver=1` are decoded as v1 session tokens, otherwise they are treated as legacy tokens.
|
||||
- Shared-key auth requires `x-shared-key` as `<key-id> <key>` and stores the lowercased key id on the request. If no shared keys are configured it always rejects.
|
||||
- Session management uses DB storage unless the DB pool is read-only, then falls back to the in-memory manager. DB sessions support both legacy string ids and v2 UUID session ids.
|
||||
- Session cookies are renewed when using a legacy string id or when `modified-at` is older than the renewal interval. SameSite is `none` for CORS, otherwise strict/lax based on config.
|
||||
|
||||
## Storage and media
|
||||
|
||||
- Storage has a fixed valid bucket set. Backends are `:fs` and `:s3`; default backend comes from deprecated `assets-storage-backend` only when present, otherwise `objects-storage-backend`, defaulting to `:fs`.
|
||||
- `put-object!` creates the DB `storage_object` row before writing backend content. Backend writes happen only for newly created rows, so deduplication can skip object writes.
|
||||
- Deduplication only applies when requested, when the content can provide a hash, and when bucket metadata is present. Reads exclude soft-deleted storage rows.
|
||||
- `sto/resolve` can reuse the current DB connection via `::db/reuse-conn true`; preserve this in transaction-sensitive code.
|
||||
- SVG validation strips DOCTYPE and uses secure SAX parsing. Basic SVG info falls back to 100x100 dimensions when width/height/viewBox are missing.
|
||||
- Raster metadata is shell-derived with ImageMagick `identify`, verifies detected MIME against the supplied MIME, and swaps dimensions for EXIF orientations 6/8.
|
||||
- Remote image download requires 2xx status, `content-length`, a known MIME, and size under the configured maximum before writing the temp file; mismatched byte count is an internal error.
|
||||
- Font processing shells out to FontForge and WOFF conversion tools and can derive TTF/OTF/WOFF variants from uploaded fonts.
|
||||
|
||||
## File data persistence
|
||||
|
||||
- File data backends are `legacy-db`, `db`, and `storage`. The storage backend keeps encoded file data in storage bucket `file-data`; the DB row stores metadata with `storage-ref-id` and nil data.
|
||||
- `fdata/upsert!` touches any storage object referenced by incoming metadata before storing the new row/blob.
|
||||
- Pointer-map fragments are persisted separately as type `fragment`, and only modified pointer maps are written.
|
||||
- `fdata/realize` combines pointer realization and object-map realization. Use it before operations that need complete in-memory file data instead of pointer placeholders.
|
||||
26
.serena/memories/backend/rpc-db-worker-subtleties.md
Normal file
26
.serena/memories/backend/rpc-db-worker-subtleties.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Backend RPC/DB/Worker Subtleties
|
||||
|
||||
## RPC exposure and wrappers
|
||||
|
||||
- RPC commands are discovered from vars created by `app.util.services/defmethod`; adding a command namespace is not enough unless `backend/src/app/rpc.clj` includes it in `resolve-methods`.
|
||||
- `GET`/`HEAD` RPC calls are only allowed for method names starting with `get-`. Other methods are method-not-allowed even if they are read-only internally.
|
||||
- RPC auth defaults to enabled. Public endpoints must set `::auth false` metadata explicitly.
|
||||
- The wrapper stack does auth before params validation, then auditing/rate/concurrency/metrics/retry/condition handling, with DB transaction handling inside that stack. `::db/transaction` metadata controls transaction wrapping.
|
||||
- Params with `::sm/params` are decoded/conformed through the JSON transformer and successful IObj results get `:encode/json` metadata. Legacy spec conforming only applies when no Malli params schema exists.
|
||||
- Nil RPC bodies become HTTP 204 unless explicit status metadata is present. Stream bodies default to `application/octet-stream` when no content type is set.
|
||||
|
||||
## DB helpers
|
||||
|
||||
- Most `app.db` helpers accept a pool, connection, or map containing `::db/pool` / `::db/conn`; preserve that convention in shared code.
|
||||
- `db/tx-run!` uses `next.jdbc.transaction/*nested-tx* :ignore`: nested transaction calls reuse the outer transaction, not a savepoint. Use explicit savepoints when nested rollback semantics matter.
|
||||
- `db/run!` opens/reuses one connection but does not create a transaction.
|
||||
- `db/tjson` is Transit JSON for jsonb storage; `db/json` is plain JSON. Worker task props use Transit and are decoded with `decode-transit-pgobject`.
|
||||
- Advisory transaction locks accept UUIDs or ints. UUID locks are hashed using a zero-UUID seeded siphash.
|
||||
|
||||
## Workers and cron
|
||||
|
||||
- Task queues are tenant-prefixed. Submit dedupe only removes not-yet-due `new` tasks with the same name/queue/label; it does not dedupe due, scheduled, retry, running, or completed work.
|
||||
- The dispatcher selects `new`/`retry` tasks with `FOR UPDATE SKIP LOCKED`, marks them `scheduled`, and publishes Redis payload `[id scheduled-at]`. The runner skips Redis messages whose scheduled timestamp no longer matches DB state.
|
||||
- Lost `scheduled` tasks are rescheduled after about 5 minutes; `running` tasks older than about 24 hours are marked failed as orphans.
|
||||
- A task handler that is missing or returns an invalid result currently defaults to completed after warning. Throwing with `ex-data :type ::retry` controls retry behavior; `:strategy ::noop` retries without incrementing retry count.
|
||||
- Cron jobs lock their `scheduled_task` row with `FOR UPDATE SKIP LOCKED`, disable statement/idle-in-transaction timeouts locally, and reschedule themselves in `finally` unless interrupted. Worker, dispatcher, and cron components do not start when the DB pool is read-only.
|
||||
39
.serena/memories/common/changes-architecture.md
Normal file
39
.serena/memories/common/changes-architecture.md
Normal file
@ -0,0 +1,39 @@
|
||||
# File Mutations: Changes and Undo Architecture
|
||||
|
||||
Penpot mutates file data through change records. A change set is both the persistence payload and the basis for undo/redo, so UI actions, tests, backend file updates, and library/file tooling should drive the production change pipeline instead of ad hoc object-map mutation.
|
||||
|
||||
## Change shape
|
||||
|
||||
Each change is a map such as `{:type ... :id ... :page-id ...}`. Common families:
|
||||
|
||||
- `:add-obj`, `:mod-obj`, `:del-obj`: shape lifecycle. `:mod-obj` contains `:operations`, commonly `{:type :set :attr ... :val ... :ignore-geometry ... :ignore-touched ...}` or `{:type :set-touched ...}`.
|
||||
- `:add-component`, `:mod-component`, `:del-component`: component/library metadata.
|
||||
- `:add-children`, `:remove-children`, `:reg-objects`: tree and object-map edits.
|
||||
- `:set-option`, `:add-page`, `:mov-page`, and related file/page metadata changes.
|
||||
|
||||
Each transaction carries `:redo-changes` and inverse `:undo-changes`. The undo stack stores transactions and can move its index backward/forward.
|
||||
|
||||
## changes-builder API
|
||||
|
||||
`common/src/app/common/files/changes_builder.cljc` (usually alias `pcb`) is the fluent builder. Start from `(pcb/empty-changes <it> <page-id>)` or `(pcb/empty-changes nil <page-id>)` for tests.
|
||||
|
||||
High-value builder operations:
|
||||
- `pcb/with-page-id`, `pcb/with-objects`, `pcb/with-library-data`: set context for following operations.
|
||||
- `pcb/update-shapes ids update-fn`: emits `:mod-obj` with diff-derived `:set` ops. Options include `{:with-objects? true}`, `{:ignore-touched true}`, and `{:attrs #{...}}`.
|
||||
- `pcb/add-objects`, `pcb/change-parent`, `pcb/remove-objects`, `pcb/resize-parents`: shape/tree edits.
|
||||
- `pcb/add-component`, `pcb/update-component`, `pcb/mod-component`: component/library edits.
|
||||
- `pcb/set-translation? true`: marks the whole change set as translation-only, which lets component sync skip expensive work.
|
||||
|
||||
## Applying changes in tests
|
||||
|
||||
`thf/apply-changes` in `app.common.test-helpers.files` is the test analog of the production applier. It validates by default; pass `:validate? false` only for intentionally-invalid intermediate states.
|
||||
|
||||
The applier uses the same `process-operation` multimethod as production (`common/src/app/common/files/changes.cljc`), so tests that use it exercise production behavior.
|
||||
|
||||
## :touched and geometry
|
||||
|
||||
For component touched semantics and sync groups, read `mem:common/component-data-model`. For the exact `set-shape-attr` / second-pass behavior during change application, read `mem:common/file-change-validation-migration-subtleties`. For transform-specific ignore-geometry behavior, read `mem:frontend/workspace-transform-subtleties`.
|
||||
|
||||
## Inspection
|
||||
|
||||
To inspect what a UI action emitted, use `mem:frontend/cljs-repl` with the snippets in `mem:common/component-debugging-recipes` rather than adding temporary source instrumentation.
|
||||
41
.serena/memories/common/component-data-model.md
Normal file
41
.serena/memories/common/component-data-model.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Component and Variant Data Model
|
||||
|
||||
## Shape roles relative to components
|
||||
|
||||
A shape can occupy multiple roles at once:
|
||||
|
||||
1. Master/main instance: defines a component and has `:main-instance true` plus `:component-id`.
|
||||
2. Copy/non-main instance: produced by instantiating a component and carries `:shape-ref` pointing at the master shape. `(ctk/in-component-copy? shape)` is essentially `(some? (:shape-ref shape))`.
|
||||
3. Component root: topmost shape of an instance, marked `:component-root true` and carrying surface attrs such as `:component-id` and `:component-file`.
|
||||
|
||||
Variant masters are main instances and component roots. Their descendants may themselves be component copies, so master/copy logic must handle nested instances rather than assuming those roles are exclusive.
|
||||
|
||||
## :shape-ref chains
|
||||
|
||||
`:shape-ref` walks up the inheritance hierarchy and can cross files for remote libraries. `find-ref-shape` and `get-ref-chain-until-target-ref` in `app.common.types.file` follow this chain.
|
||||
|
||||
`find-shape-ref-child-of` in `app.common.logic.variants` walks the chain looking for the first ref-shape whose ancestors include a specific parent. Variant switch uses this to locate the equivalent master child in the target variant.
|
||||
|
||||
## :touched flags
|
||||
|
||||
`:touched` is a set of override-group keywords such as `:geometry-group`, `:fill-group`, and `:text-content-group`. It means a copy diverged from its master for attrs in that sync group.
|
||||
|
||||
`sync-attrs` in `app.common.types.component` maps attrs to groups. `set-touched-group` is the legitimate setter; the central `set-shape-attr` path calls it only for copies and only when ignore flags allow it.
|
||||
|
||||
Masters are not normally touched through `set-shape-attr`, but touched flags can appear on master shapes through cloning/duplication paths. `add-touched-from-ref-chain` in `app.common.logic.variants` unions touched flags from ancestors into the copy being processed, so upstream/master touched state can affect downstream switch behavior.
|
||||
|
||||
## Cloning paths
|
||||
|
||||
`make-component-instance` in `app.common.types.container` produces a clean component copy through `update-new-shape`, dissociating attrs such as `:touched`, `:variant-id`, and `:variant-name` on cloned shapes.
|
||||
|
||||
`duplicate-component` in `app.common.logic.libraries` creates a new component master by cloning existing component shapes, setting component metadata, and applying a position delta. It does not have the same clean-copy semantics as `make-component-instance`, so inherited attrs on the source can matter.
|
||||
|
||||
When a bug depends on touched state, identify which cloning path produced the shape before changing sync logic.
|
||||
|
||||
## Variant containers
|
||||
|
||||
A variant container is a frame with `:is-variant-container true`. Its children are variant masters with `:variant-id` pointing at the container and `:variant-name` naming the variant value. Component records in the library carry `:variant-properties`.
|
||||
|
||||
Predicates are broad: `ctk/is-variant?` checks `:variant-id` and applies to both variant master shapes and component rows; `ctk/is-variant-container?` checks the container shape flag.
|
||||
|
||||
Moving/dropping a shape into a variant container through the move-to-frame path can auto-convert it into a variant via `generate-make-shapes-variant`, which may duplicate the underlying component. Treat drag/drop into variant containers as a component/variant operation, not a plain reparent.
|
||||
58
.serena/memories/common/component-debugging-recipes.md
Normal file
58
.serena/memories/common/component-debugging-recipes.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Common Component and Change Debugging Recipes
|
||||
|
||||
Keep source changes out of these recipes unless the task requires a durable fix.
|
||||
|
||||
## Inspect recent workspace changes
|
||||
|
||||
From `cljs_repl` after triggering an action:
|
||||
|
||||
```clojure
|
||||
(let [items (get-in @app.main.store/state [:workspace-undo :items])
|
||||
n (count items)]
|
||||
(->> items
|
||||
(drop (max 0 (- n 5)))
|
||||
(map-indexed (fn [i it]
|
||||
{:idx (+ i (max 0 (- n 5)))
|
||||
:tags (:tags it)
|
||||
:n (count (:redo-changes it))
|
||||
:types (frequencies (map :type (:redo-changes it)))
|
||||
:ids (mapv :id (:redo-changes it))}))))
|
||||
```
|
||||
|
||||
To inspect operations within the latest `:mod-obj`:
|
||||
|
||||
```clojure
|
||||
(let [items (get-in @app.main.store/state [:workspace-undo :items])
|
||||
mod-obj (->> (:redo-changes (last items))
|
||||
(filter #(= :mod-obj (:type %)))
|
||||
first)]
|
||||
(:operations mod-obj))
|
||||
```
|
||||
|
||||
## Trace variant switch attribute copying
|
||||
|
||||
To capture what `update-attrs-on-switch` saw during a real UI swap, patch it temporarily in `cljs_repl`:
|
||||
|
||||
```clojure
|
||||
(def orig (deref #'app.common.logic.libraries/update-attrs-on-switch))
|
||||
(def trace-buf (atom []))
|
||||
(set! app.common.logic.libraries/update-attrs-on-switch
|
||||
(fn [& args]
|
||||
(swap! trace-buf conj
|
||||
(let [[_ curr prev _ _ origin _] args]
|
||||
{:curr (select-keys curr [:name :x :y :selrect :points :touched])
|
||||
:prev (select-keys prev [:name :x :y :selrect :points :touched])
|
||||
:origin-ref (select-keys origin [:id :name :x :y :width :height :selrect])}))
|
||||
(apply orig args)))
|
||||
;; trigger UI action, then inspect @trace-buf
|
||||
(set! app.common.logic.libraries/update-attrs-on-switch orig)
|
||||
```
|
||||
|
||||
Runtime patching is faster than adding temporary source instrumentation and avoids recompilation cleanup. Restore the var or reload the frontend when finished.
|
||||
|
||||
## Test-side helpers
|
||||
|
||||
- Use `thf/dump-file file :keys [...]` to print a shape tree with selected keys during common tests.
|
||||
- Prefer production-path helpers such as `cls/generate-update-shapes` plus `thf/apply-changes` for shape mutations.
|
||||
- For component swaps with keep-touched behavior, use `tho/swap-component-in-shape` with `{:keep-touched? true}`.
|
||||
- Temporary `prn` calls in production code are acceptable while investigating but should be removed before committing.
|
||||
42
.serena/memories/common/component-swap-pipeline.md
Normal file
42
.serena/memories/common/component-swap-pipeline.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Component Swap and Variant Switch Pipeline
|
||||
|
||||
## Entry points
|
||||
|
||||
Frontend entry points under `frontend/src/app/main/data/workspace/`:
|
||||
- `variants.cljs`: `variants-switch` and `variant-switch` events feed property-toggle UI and Plugin API `switchVariant` behavior into `dwl/component-swap`.
|
||||
- `libraries.cljs`: `component-swap` is the single-swap workhorse; `component-multi-swap` batches swaps and calls `component-swap` with `keep-touched? = false`.
|
||||
|
||||
`keep-touched? = true` is the discriminator for preserving user overrides during variant switch. Batch/multi-swap paths intentionally bypass that logic.
|
||||
|
||||
## Common-side pipeline
|
||||
|
||||
For a single swap with `keep-touched? = true`:
|
||||
|
||||
1. `cll/generate-component-swap` in `common/src/app/common/logic/libraries.cljc` builds the base changes: remove old shape and instantiate the target component in its place through `generate-new-shape-for-swap`, `generate-instantiate-component`, and `make-component-instance`.
|
||||
2. `clv/generate-keep-touched` in `common/src/app/common/logic/variants.cljc` walks pre-swap children, augments each with chain-derived touched flags through `add-touched-from-ref-chain`, finds the equivalent target shape through `find-shape-ref-child-of`, then calls `update-attrs-on-switch`.
|
||||
3. `update-attrs-on-switch` in `app.common.logic.libraries` decides which touched attrs from the previous shape should be copied onto the freshly instantiated target shape.
|
||||
|
||||
## update-attrs-on-switch hazards
|
||||
|
||||
The routine compares `current-shape` (fresh target copy), `previous-shape` (pre-swap shape with chain-derived touched), and `origin-ref-shape` (source variant master's equivalent shape). It loops over sync attrs except `swap-keep-attrs` and copies only attrs that pass several guards:
|
||||
|
||||
- skip equal previous/current values;
|
||||
- skip equal composite geometry for selected attrs;
|
||||
- require the corresponding touched group;
|
||||
- for most attrs, require source and target masters to agree;
|
||||
- for fixed-size selrect/points/width/height, use dedicated fixed-layout geometry handling;
|
||||
- text and path shapes have specialized value conversion paths.
|
||||
|
||||
The generic fallback branch copies from `previous-shape`. It represents the intended "carry user override through switch" behavior, but bugs usually appear when guards fail to reject incompatible geometry or master differences before reaching that branch.
|
||||
|
||||
## Known sharp edges
|
||||
|
||||
- Composite `:selrect` and `:points` bypass the simple different-master skip; width/height checks catch some but not all positional differences.
|
||||
- `previous-shape` may be repositioned by destination-root minus origin-root before copying. For normal variant switch this is often zero, but do not assume it for all swap entry points.
|
||||
- Touched flags can be inherited through ref chains, so a shape that looks untouched locally may still behave as touched after `add-touched-from-ref-chain`.
|
||||
|
||||
## Test harness
|
||||
|
||||
`common/src/app/common/test_helpers/compositions.cljc` has `swap-component-in-shape`, which drives `generate-component-swap` plus `generate-keep-touched` with the production `keep-touched?` flag. Use it for focused common tests of variant-switch behavior.
|
||||
|
||||
`common/test/common_tests/logic/variants_switch_test.cljc` is the canonical reference suite for swap+touched scenarios. Read nearby tests before adding another case.
|
||||
55
.serena/memories/common/core.md
Normal file
55
.serena/memories/common/core.md
Normal file
@ -0,0 +1,55 @@
|
||||
# Common Architecture and Workflow
|
||||
|
||||
`common/` intro: shared CLJC for frontend, backend, exporter, library/file tooling, tests. Small semantic changes can affect multiple runtimes.
|
||||
|
||||
## Stable namespace map
|
||||
|
||||
- `app.common.data` and `app.common.data.macros`: generic data helpers and performance macros that do not depend on Penpot domain entities.
|
||||
- `app.common.types.*`: shared shape/file/page/component/token data types, schemas, predicates, and entity-local operations. `app.common.types.nitrate-permissions` contains shared fail-closed Nitrate organization/team permission rules.
|
||||
- `app.common.files.*`: file-level operations, shape tree helpers, change application, migrations, validation, and undo/redo-related logic.
|
||||
- `app.common.logic.*`: higher-level workflows/algorithms over files, shapes, components, variants, libraries, tokens, etc.
|
||||
- `app.common.geom.*`: geometry helpers and transformations.
|
||||
- `app.common.schema` / `app.common.schema.*`: Malli abstraction layer.
|
||||
- `app.common.math`, `app.common.time`, `app.common.uuid`, `app.common.json`, etc.: cross-runtime utilities.
|
||||
- `app.common.test_helpers.*`: test builders and production-path helpers.
|
||||
|
||||
## Layering and cross-runtime rules
|
||||
|
||||
Use reader conditionals for platform-specific code. Because CLJC runs on JVM and CLJS targets, avoid assuming browser-only or JVM-only behavior unless the reader conditional isolates it.
|
||||
|
||||
Respect the intended abstraction direction in new/refactored code:
|
||||
- generic data utilities should not know Penpot domain concepts;
|
||||
- `types.*` should preserve invariants for a single domain entity or ADT;
|
||||
- `files.*` can coordinate several entities inside a file and preserve referential integrity;
|
||||
- `changes*` should adapt serializable change records to lower-level operations and avoid embedding broad business algorithms;
|
||||
- `logic.*` and frontend/backend event layers own higher workflow/business behavior.
|
||||
|
||||
Some legacy code violates this layering; do not copy those violations into new code when a focused refactor is practical.
|
||||
|
||||
## Focused memory routing
|
||||
|
||||
Model, schema, and persistence shape:
|
||||
- File/page/shape/component attr changes, import/export surfaces, inspector/codegen, and cross-module checklist: `mem:common/data-model-change-checklist`.
|
||||
- Token data structures, token import/export, active theme/set semantics, and schema/coercion behavior: `mem:common/tokens-schema-subtleties`.
|
||||
|
||||
Geometry and layout:
|
||||
- Shape geometry invariants, redundant geometry fields, and geometry-sensitive tests: `mem:common/geometry-invariants`.
|
||||
- Coordinate drift and approximate float comparisons: `mem:common/decimals-and-coordinates`.
|
||||
- Layout/grid assignment, deassignment, metadata cleanup, and auto-positioning: `mem:common/layout-grid-subtleties`.
|
||||
|
||||
Change pipeline, validation, and migrations:
|
||||
- Change records, undo/redo architecture, changes-builder API, and production-path mutation guidance: `mem:common/changes-architecture`.
|
||||
- Change application, shape-tree edits, validation/repair, migrations, and second-pass touched behavior: `mem:common/file-change-validation-migration-subtleties`.
|
||||
|
||||
Components, variants, and debugging:
|
||||
- Component/variant data model, ref chains, touched override semantics, and cloning paths: `mem:common/component-data-model`.
|
||||
- Component swap, variant switch, and keep-touched pipeline: `mem:common/component-swap-pipeline`.
|
||||
- Live inspection snippets, temporary runtime patching, and test-side debugging helpers for common change/component behavior: `mem:common/component-debugging-recipes`.
|
||||
|
||||
Text and tests:
|
||||
- Shared text data conversion, DraftJS compatibility, modern text content, and derived position data: `mem:common/text-subtleties`.
|
||||
- Common test commands, helper conventions, production-path test mutations, and runtime coverage choices: `mem:common/test-setup`.
|
||||
|
||||
## Areas without focused memories
|
||||
|
||||
Common areas with little or no dedicated memory include colors, media/SVG helpers, path operations, thumbnail helpers, generic pools, weak refs, and some utility namespaces. Treat work there as source/test-led unless a focused memory exists.
|
||||
25
.serena/memories/common/data-model-change-checklist.md
Normal file
25
.serena/memories/common/data-model-change-checklist.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Common Data Model Change Checklist
|
||||
|
||||
## Attribute conventions
|
||||
|
||||
- Prefer optional page/shape attrs with default behavior when absent. Reverting to default should usually remove the attr instead of storing nil.
|
||||
- Do not treat nil as a distinct persisted state from absence. Import/export and cleanup paths may filter nil attrs away.
|
||||
- Avoid Clojure-special naming in exported object attrs, especially boolean names ending in `?`; exported/imported data must survive JSON/SVG/Transit and external tooling.
|
||||
- Any new shape attr that participates in component sync must be listed in `app.common.types.component/sync-attrs` with the correct touched group. Attrs absent from `sync-attrs` are ignored by component synchronization.
|
||||
|
||||
## Cross-module update checklist
|
||||
|
||||
When changing the file data model, check the relevant paths:
|
||||
- Schema/type definitions under `common/src/app/common/types*` and helpers under `common/src/app/common/files*` / `logic*`.
|
||||
- File migrations in `common/src/app/common/files/migrations.cljc` when old files cannot safely use absence/default behavior.
|
||||
- Frontend edit forms under `frontend/src/app/main/ui/workspace/sidebar/options/`; multi-selection behavior is usually in `multiple.cljs` and must handle `:multiple` values.
|
||||
- SVG/file render and export metadata under `frontend/src/app/main/ui/shapes/*`, especially `export.cljs` when an attr is not a native SVG property.
|
||||
- SVG import/parser paths under `frontend/src/app/worker/import/parser.cljs`; attrs not exported and imported will be lost on reimport.
|
||||
- Viewer inspect and code generation under `frontend/src/app/main/ui/viewer/inspect/*` and `frontend/src/app/util/code_gen.cljs` / markup/style helpers when handoff output should expose the attr.
|
||||
- Exporter/library consumers when the change affects file construction, rendering, or packaged `.penpot` archives.
|
||||
|
||||
## Migrations
|
||||
|
||||
Existing files should keep working unchanged when possible. If absence cannot preserve old behavior, add a migration and preserve append/order semantics described in `mem:common/file-change-validation-migration-subtleties`.
|
||||
|
||||
Model changes can also require file feature flags or migration metadata updates; check nearby migrations and `common/src/app/common/features.cljc` before inventing a new pattern.
|
||||
54
.serena/memories/common/decimals-and-coordinates.md
Normal file
54
.serena/memories/common/decimals-and-coordinates.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Decimals and Coordinates in Penpot
|
||||
|
||||
Penpot stores all geometry as JS numbers (doubles in CLJS, doubles in
|
||||
CLJ for the JVM-side common code). Several Penpot-specific facts
|
||||
about how this plays out are not obvious from reading the code.
|
||||
|
||||
## Sub-pixel drift is routine
|
||||
|
||||
Coordinate values that "should" be integers are routinely off by ~1e-5
|
||||
in production data. A `:width` of 107 will frequently appear as
|
||||
`107.00001275539398` after the value has passed through:
|
||||
|
||||
- the modifier propagation pipeline (`apply-wasm-modifiers` and the
|
||||
Rust WASM transform engine)
|
||||
- any rotation/scale composition
|
||||
- repeated translations
|
||||
|
||||
This drift is invisible in the UI (the renderer rounds at draw time)
|
||||
but defeats exact equality comparisons in business logic. It does NOT
|
||||
appear in JVM-only test setups because the WASM pipeline isn't
|
||||
involved — tests that build shapes via `setup-shape` and `add-sample-shape`
|
||||
get clean integer values. Bugs that depend on drift will pass tests
|
||||
but fire in production unless tests explicitly inject drift.
|
||||
|
||||
## Use the close? helpers, not `=`
|
||||
|
||||
For comparing coordinate-like floats, the established convention is:
|
||||
|
||||
- `app.common.math/close?` — scalar tolerance comparison.
|
||||
Default precision 0.001 (sub-pixel; tight enough to keep distinct
|
||||
shapes distinct, loose enough to absorb arithmetic noise).
|
||||
Two-arity uses default precision; three-arity takes a custom one.
|
||||
- `app.common.geom.point/close?` — element-wise close on `gpt/Point`
|
||||
records. Compares :x and :y via `mth/close?`.
|
||||
- `app.common.geom.matrix/close?` — close on transform matrices.
|
||||
- `app.common.geom.shapes/close-attrs?` — used inside `set-shape-attr`
|
||||
to decide whether a re-assigned `:width`/`:height` should be treated
|
||||
as a no-op (suppresses spurious touched marking from drift).
|
||||
|
||||
Treat `=` on `:x`, `:y`, `:width`, `:height`, `:selrect`, or `:points`
|
||||
fields as a code smell when the inputs may have flowed through any
|
||||
transform. The `set-shape-attr`-style precedent (already using
|
||||
`close-attrs?`) is the right model.
|
||||
|
||||
## The redundancy multiplies failure modes
|
||||
|
||||
A shape's position lives in `:x/:y`, `:selrect`, AND `:points` (see
|
||||
`mem:common/geometry-invariants` memory). Each is a separate set of float
|
||||
values. After any operation that touches geometry, all three should
|
||||
agree, but each is computed by a different path and accumulates
|
||||
its own drift. Comparing `:selrect.width` from shape A to
|
||||
`:selrect.width` from shape B is comparing two values that
|
||||
"semantically" should be equal but were computed via different
|
||||
operation chains — exact equality will often be false.
|
||||
@ -0,0 +1,28 @@
|
||||
# Common File Change, Validation, and Migration Subtleties
|
||||
|
||||
## Change application
|
||||
|
||||
- `process-changes` validates the whole change vector once by default, reduces changes, then performs a second pass for collected touched changes. Callers that already validated can pass `verify? false`.
|
||||
- `process-operation :set` delegates to `ctn/set-shape-attr`; `:assign` first decodes attrs with the shape-attrs JSON transformer and then emits per-attr set operations.
|
||||
- `set-shape-attr` treats `:position-data` as derived and never touched. Geometry/content-path changes use approximate equality; geometry differences under about 1px can be ignored for touched purposes.
|
||||
- Width/height are excluded from the `is-geometry?` branch in `set-shape-attr`; do not assume all geometry-group attrs follow identical ignore-geometry behavior.
|
||||
- `process-touched-change` marks the owning component modified when a touched shape belongs to a main instance; component-data changes can come from shape ops through this second pass.
|
||||
|
||||
## Shape tree edits
|
||||
|
||||
- `shape-tree/add-shape` falls back invalid/missing parent or frame ids to root (`uuid/zero`), ensures parent `:shapes` is a vector, avoids duplicate child ids, and clears `:remote-synced` on copy parents unless `ignore-touched` is true.
|
||||
- `shape-tree/delete-shape` removes the shape and all descendants from the objects map and removes the id from its parent. This is different from render-wasm deletion, which may keep deleted children for undo/redo internals.
|
||||
- Page object maps can carry metadata indexes such as cached frame lists. `start-page-index` / `update-page-index` rebuild those metadata indexes; `frontend` commit application calls `ctst/update-object-indices` after page changes.
|
||||
|
||||
## Validation and repair
|
||||
|
||||
- Full referential/semantic validation currently runs only when file features contain `"components/v2"`.
|
||||
- Validation starts at root plus orphan shapes, then validates component records. `validate-file!` raises `:validation :referential-integrity` with collected details.
|
||||
- `repair-file` does not mutate data directly; it reduces validation errors into redo changes using `changes-builder`. Callers must apply or persist those changes.
|
||||
|
||||
## Migrations
|
||||
|
||||
- Prefer optional attrs/default behavior so old files continue working without migration. If absence cannot preserve old behavior, add a migration.
|
||||
- Migrations are an ordered set mixing legacy version-derived ids and newer named ids. Keep append order stable; `migrate` applies the set difference between available migrations and file migrations.
|
||||
- `migrate-file` synthesizes legacy migration ids from old numeric versions when `:migrations` is absent, migrates legacy features, and records feature flags created through `cfeat/*new*`.
|
||||
- When a file had no previous `:migrations`, `migrate-file` marks all migrations as migrated in metadata so callers persist the complete migration set, not only transformations that changed data.
|
||||
48
.serena/memories/common/geometry-invariants.md
Normal file
48
.serena/memories/common/geometry-invariants.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Geometry Invariants in Penpot Shapes
|
||||
|
||||
Core invariant: shape position is stored redundantly, and all geometry fields must stay coherent.
|
||||
|
||||
## Redundant fields
|
||||
|
||||
For a shape at `(x, y)` with width `w` and height `h`:
|
||||
|
||||
- `:x`, `:y`, `:width`, `:height`: top-left and dimensions.
|
||||
- `:selrect`: `{:x :y :width :height :x1 :y1 :x2 :y2}`, where `x2 = x + w` and `y2 = y + h`.
|
||||
- `:points`: four corners for an axis-aligned rect, clockwise from top-left.
|
||||
- `:transform` and `:transform-inverse`: identity for axis-aligned shapes; populated for transformed shapes.
|
||||
|
||||
After a geometric mutation, equivalent fields such as `:y`, `(:y :selrect)`, and the first point's `:y` should agree. The renderer and hit-testing read `:selrect` / `:points`, so a shape can render or select incorrectly even when `:x` / `:y` look right.
|
||||
|
||||
## Helpers that preserve the invariant
|
||||
|
||||
- `gsh/move`: translates by delta and updates geometry consistently.
|
||||
- `gsh/absolute-move`: moves to an absolute position by computing a delta from the current selrect.
|
||||
- `gsh/transform-shape`: applies a full transform.
|
||||
- `cts/setup-shape`: initializes geometry for new shapes; variant test helpers such as `thv/add-variant-with-child` use it.
|
||||
|
||||
## Edits that break the invariant
|
||||
|
||||
- `(assoc shape :x ...)` or `(assoc shape :y ...)`: updates only one field and leaves `:selrect` / `:points` stale.
|
||||
- `ths/update-shape file label :y val`: goes through `set-shape-attr`, but does not repair all position fields for `:y` alone.
|
||||
- Direct `update-in` edits to `:selrect`, `:points`, or dimensions.
|
||||
|
||||
## Test setup warning
|
||||
|
||||
When positioning test shapes, use `gsh/absolute-move`, `gsh/move`, or production change helpers. Do not set only `:x` / `:y`.
|
||||
|
||||
```clojure
|
||||
(cls/generate-update-shapes
|
||||
(pcb/empty-changes nil page-id)
|
||||
#{(:id child)}
|
||||
#(gsh/absolute-move % (gpt/point (:x %) 101))
|
||||
(:objects page)
|
||||
{})
|
||||
```
|
||||
|
||||
Using `(ths/update-shape file label :y 101)` leaves `:selrect.y` stale. Downstream code that reads `:selrect` can then fail in ways that look like product bugs but are only invalid test setup.
|
||||
|
||||
## :touched and geometry mutation
|
||||
|
||||
When a copy shape changes geometry through the proper pipeline (`set-shape-attr` via `process-operation :set`), `:touched` gains `:geometry-group` unless ignored. Tests can either drive the production update with `cls/generate-update-shapes`, or inject `(assoc shape :touched #{:geometry-group})` when only touched state matters.
|
||||
|
||||
If a test needs both a new position and touched state, move the shape first with geometry-preserving helpers, then inject or assert touched state.
|
||||
13
.serena/memories/common/layout-grid-subtleties.md
Normal file
13
.serena/memories/common/layout-grid-subtleties.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Common Layout and Grid Subtleties
|
||||
|
||||
## Layout metadata
|
||||
|
||||
- Layout container data and child layout-item data are removed by different helpers. Do not assume clearing a layout frame also clears all child layout metadata.
|
||||
- Layout data can affect both container attrs and immediate child attrs; validate behavior for both sides when changing cleanup or propagation.
|
||||
|
||||
## Grid assignment
|
||||
|
||||
- Grid `assign-cells` ensures at least one column and row, skips absolute-position children, creates non-tracked rows/cols when children exceed tracked cells, and asserts that assigned cells do not overlap.
|
||||
- Grid deassignment removes cells for shapes that are no longer direct children or have become absolute-positioned.
|
||||
- Auto-positioning is not just sorting: some auto cells are converted to manual when empty/manual/span state would break the auto sequence, then auto single-span items can be compacted.
|
||||
- `fix-overlaps` is marked dev-only and removes one overlapping cell, preferring empty cells first. Avoid depending on it as normal production repair.
|
||||
45
.serena/memories/common/test-setup.md
Normal file
45
.serena/memories/common/test-setup.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Common Module Test Setup
|
||||
|
||||
`common/` is CLJC shared code. Tests should cover the relevant runtime(s): JVM for backend/common logic and JS for frontend/exporter behavior. For geometry, component, and file-model changes, JVM tests are common and fast, but JS/browser behavior can differ when WASM modifier math or CLJS-specific state is involved.
|
||||
|
||||
## Running tests
|
||||
|
||||
From `common/`:
|
||||
|
||||
```bash
|
||||
pnpm run test:jvm
|
||||
clojure -M:dev:test
|
||||
pnpm run test:jvm --focus common-tests.logic.variants-switch-test
|
||||
clojure -M:dev:test --focus common-tests.logic.variants-switch-test/test-basic-switch
|
||||
pnpm run test:js
|
||||
pnpm run watch:test
|
||||
```
|
||||
|
||||
Focused JS tests are selected by editing `test/common_tests/runner.cljs`, then running `pnpm run test:js`. Multiple JVM `--focus` flags compose as a union.
|
||||
|
||||
## Test helpers
|
||||
|
||||
Helpers live under `common/src/app/common/test_helpers/` and are usually aliased with short `th*` prefixes. Test namespaces using label->uuid helpers should start with `(t/use-fixtures :each thi/test-fixture)` so labels reset between tests.
|
||||
|
||||
Useful builders:
|
||||
- `thf/sample-file` creates a base file.
|
||||
- `tho/add-simple-component` creates a simple component.
|
||||
- `thc/instantiate-component` instantiates a component copy.
|
||||
- `thv/add-variant-with-child` creates a variant container with two child variants.
|
||||
- `thv/add-variant-with-copy` creates variants whose children are component instances.
|
||||
|
||||
`add-variant-with-copy` does not accept position params for children; use `gsh/absolute-move` after creation if positions matter.
|
||||
|
||||
## Driving production paths
|
||||
|
||||
For shape mutations, prefer production-path helpers such as `cls/generate-update-shapes` plus `thf/apply-changes`. For component swaps with keep-touched behavior, use `tho/swap-component-in-shape` with `{:keep-touched? true}`.
|
||||
|
||||
`thf/apply-changes` validates by default and usually gives the most useful invariant failure. Pass `:validate? false` only for intentionally malformed intermediate state.
|
||||
|
||||
## Geometry setup caution
|
||||
|
||||
For geometry-sensitive tests, read `mem:common/geometry-invariants` before positioning shapes. Use geometry-preserving helpers or production change helpers rather than direct single-field edits.
|
||||
|
||||
## Debugging
|
||||
|
||||
Use `mem:common/component-debugging-recipes` for shape-tree dumps, undo/change inspection, and temporary live instrumentation recipes.
|
||||
14
.serena/memories/common/text-subtleties.md
Normal file
14
.serena/memories/common/text-subtleties.md
Normal file
@ -0,0 +1,14 @@
|
||||
# Common Text Subtleties
|
||||
|
||||
## DraftJS compatibility
|
||||
|
||||
- `app.common.text` is legacy DraftJS conversion support. New text work should prefer the newer text type namespaces unless specifically touching DraftJS conversion.
|
||||
- DraftJS style values are encoded as Transit strings under `PENPOT$$$<key>$$$<encoded>` style names. `PENPOT_SELECTION` is a special marker.
|
||||
- Text conversion uses Unicode code points on both CLJ and CLJS paths, not UTF-16 code units. This matters for offsets around emoji and astral characters.
|
||||
- Draft conversion fixes gradient type strings back to keywords.
|
||||
|
||||
## Modern text content
|
||||
|
||||
- Modern text content schema is narrow: root -> paragraph-set -> paragraph -> text nodes.
|
||||
- `position-data` is derived layout/geometry/font fragment data and should be treated as generated state, not source-of-truth file data.
|
||||
- Token propagation and some non-current-page text updates drop `:position-data` so it can be regenerated in the right runtime context.
|
||||
17
.serena/memories/common/tokens-schema-subtleties.md
Normal file
17
.serena/memories/common/tokens-schema-subtleties.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Common Tokens and Schema Subtleties
|
||||
|
||||
## Tokens
|
||||
|
||||
- `TokensLib` always ensures an internal hidden theme exists and defaults active themes to that hidden theme path. That hidden theme represents the UI state where active sets are controlled without modifying a user-created theme.
|
||||
- `get-tokens-in-active-sets` merges tokens from sets selected by active themes in set order, so later active sets with the same token name override earlier ones.
|
||||
- Activating a real theme in `common.logic.tokens` removes the hidden theme from the active-theme set unless it is the only active theme. Toggling active sets directly copies current active sets into the hidden theme.
|
||||
- DTCG import/export deliberately hides the internal hidden theme: exports omit it from `$themes` and activeThemes, while `activeSets` records the hidden/current active sets.
|
||||
- Single-set DTCG/legacy imports throw if no supported tokens are found. Multi-set import normalizes set names, keeps `tokenSetOrder`, rejects conflicting token path names, discards unsupported token types, and validates theme sets against existing sets.
|
||||
- Token values stored on shapes are token names in `:applied-tokens`, not token ids. Renames and group renames must update those name paths.
|
||||
- Token serialization has both Transit handlers for frontend/backend transport and Fressian handlers for internal file-data storage, with migrations for older token-lib internal versions.
|
||||
|
||||
## Schema
|
||||
|
||||
- `app.common.schema/json-transformer` has custom map-of key decoding/encoding, so map keys can be transformed based on the key schema instead of only the value schema.
|
||||
- `check-fn` throws `ex-info` with default `:type :assertion`, `:code :data-validation`, and `::explain`. Prefer reusable `check-fn`/lazy validators in hot or repeated paths; `sm/check` creates a checker every call.
|
||||
- `coercer` decodes with the JSON transformer and then checks. This is the common pattern for accepting external JSON-shaped data into internal types.
|
||||
@ -1,32 +0,0 @@
|
||||
# Creating Commits
|
||||
|
||||
## Message Format
|
||||
|
||||
```
|
||||
:emoji: Subject line (imperative, capitalized, no period, ≤70 chars)
|
||||
|
||||
Body (clear, concise description)
|
||||
|
||||
Co-authored-by: <You (the LLM)>
|
||||
```
|
||||
|
||||
## Commit Type Emojis
|
||||
|
||||
`:bug:` bug fix · `:sparkles:` enhancement · `:tada:` new feature · `:recycle:` refactor · `:lipstick:` cosmetic · `:ambulance:` critical fix · `:books:` docs · `:construction:` WIP · `:boom:` breaking · `:wrench:` config · `:zap:` perf · `:whale:` docker · `:paperclip:` other · `:arrow_up:` dep upgrade · `:arrow_down:` dep downgrade · `:fire:` removal · `:globe_with_meridians:` translations · `:rocket:` epic/highlight
|
||||
|
||||
## Changelog (CHANGES.md)
|
||||
|
||||
Update `CHANGES.md` for user-facing or notable changes. Add entry under the current unreleased version in the matching section (`### :boom:`, `### :sparkles:`, `### :bug:`, etc.).
|
||||
|
||||
Entry format:
|
||||
```
|
||||
- Description of change [Taiga #NNNN](https://tree.taiga.io/project/penpot/us/NNNN)
|
||||
```
|
||||
or for GitHub issues/PRs:
|
||||
```
|
||||
- Description of change [Github #NNNN](https://github.com/penpot/penpot/issues/NNNN)
|
||||
```
|
||||
|
||||
Changes that affect the JavaScript plugin API must additionally be documented in `plugins/CHANGELOG.md`:
|
||||
* Add an entry at the top of the file (unreleased section)
|
||||
* Prefix entries that change the types/signatures in the API with `**plugin-types:**` and changes affecting the runtime with `**plugin-runtime:**`.
|
||||
@ -1,30 +0,0 @@
|
||||
# Creating Pull Requests
|
||||
|
||||
Important: Before creating a PR, ensure that you are on a branch that is specific to the
|
||||
issue or feature you are working on. If necessary, create a new branch.
|
||||
|
||||
## Title Format
|
||||
|
||||
PR titles follow the same convention as commit titles:
|
||||
|
||||
```
|
||||
:emoji: Subject line (imperative, capitalized, no period, ≤70 chars)
|
||||
```
|
||||
|
||||
See the `creating-commits` memory for the list of emoji codes.
|
||||
|
||||
## Description Format
|
||||
|
||||
The PR description must start with the following notice:
|
||||
|
||||
> **Note:** This PR was created with AI assistance as part of the Penpot MCP self-improvement initiative.
|
||||
|
||||
**Related Issues** section with a bullet list of linked issues:
|
||||
|
||||
```
|
||||
In addition to sections summarising and explaining the changes in the PR, it should contain a section 'Relevant Issues' with a bullet list:
|
||||
|
||||
- Fixes #NNNN
|
||||
- Resolves #NNNN
|
||||
- Relates to #NNNN
|
||||
```
|
||||
@ -1,27 +1,54 @@
|
||||
You are working on the GitHub project penpot/penpot.
|
||||
You are working on the GitHub project `penpot/penpot`, a monorepo.
|
||||
|
||||
# Working with Penpot Designs via the JavaScript API
|
||||
# Memory system
|
||||
|
||||
Before working with Penpot designs, call the `high_level_overview` tool of the Penpot MCP server.
|
||||
It explains the API, which you can use to automate tasks via the `execute_code` tool.
|
||||
- Memories are the primary project guidance (not docs or other readme files).
|
||||
- A section's top-level memory is `<section>/core`. When a section is relevant, read the core memory
|
||||
before focused memories.
|
||||
- Edits/stale refs/duplication cleanup: `mem:memory-maintenance`.
|
||||
|
||||
# Dev Workflow
|
||||
# Development workflow
|
||||
|
||||
Memories:
|
||||
- before creating a commit, read `creating-commits`.
|
||||
- before creating a PR, read `creating-prs`.
|
||||
- Commit only when explicitly asked. Commit/PR format + changelog: `mem:workflow/creating-commits`, `mem:workflow/creating-prs`.
|
||||
- You have access to the GitHub CLI `gh` or corresponding MCP tools.
|
||||
- Issues are also managed on Taiga. Read issues using the `read_taiga_issue` tool.
|
||||
|
||||
# Frontend
|
||||
# Project modules
|
||||
|
||||
Read the file `frontend/AGENTS.md` for an overview.
|
||||
Memories:
|
||||
- connection between the JavaScript API and the ClojureScript code: `frontend/js-api-to-cljs-binding`.
|
||||
- executing ClojureScript code in the frontend: `frontend/cljs-repl`.
|
||||
- programmatically navigating to a file in the workspace: `frontend/navigation`.
|
||||
- handling Clojure compiler errors, runtime patching and debug helpers: `frontend/handling-errors-and-debugging`.
|
||||
This is a monorepo. Principles that apply to one module do *not* generally apply to others. Do not make assumptions.
|
||||
|
||||
- `frontend/`: ClojureScript + SCSS SPA/design editor.
|
||||
- `backend/`: JVM Clojure HTTP/RPC server with PostgreSQL, Redis, storage, mail, and workers.
|
||||
- `common/`: shared CLJC data types, geometry, schemas, file/change logic, and utilities.
|
||||
- `render-wasm/`: Rust -> WebAssembly Skia renderer consumed by frontend.
|
||||
- `exporter/`: ClojureScript/Node headless Playwright SVG/PDF export.
|
||||
- `mcp/`: TypeScript Model Context Protocol integration.
|
||||
- `plugins/`: TypeScript plugin runtime/examples and Plugin API types.
|
||||
- `library/`: design library workflows.
|
||||
- `docs/`: documentation site.
|
||||
|
||||
# Low-centrality project paths
|
||||
|
||||
- `docker/` contains devenv related code, not needed unless specifically instructed.
|
||||
More info in docs/technical-guide if instructed to work on this.
|
||||
- `experiments/` contains standalone experimental HTML/JS/scripts; treat it as non-core unless the user explicitly asks about it.
|
||||
- `sample_media/` contains sample image/icon media and config used as fixtures/demo material; do not infer app behavior from it.
|
||||
|
||||
# Dependency graph
|
||||
|
||||
`frontend -> common`, `backend -> common`, `exporter -> common`, and `frontend -> render-wasm`. Changes in `common` can
|
||||
affect frontend, backend, exporter, file migrations, and design-library behavior; validate across consumers when
|
||||
semantics change.
|
||||
|
||||
# Working with Penpot designs
|
||||
|
||||
- Before automating or inspecting Penpot designs through the Plugin API, call the Penpot MCP `high_level_overview` tool.
|
||||
- connection between the JavaScript plugin API and the ClojureScript code: `mem:frontend/plugin-api-to-cljs-binding`.
|
||||
- executing ClojureScript code in the frontend: `mem:frontend/cljs-repl`.
|
||||
- handling Clojure compiler errors, runtime patching and debug helpers: `mem:frontend/handling-errors-and-debugging`.
|
||||
|
||||
## Detecting Crashes
|
||||
|
||||
The Penpot frontend can crash silently from the JS API's perspective: `execute_code` calls return successfully, but 1-2s later the workspace becomes unusable (Internal Error page).
|
||||
The `execute_code` tool then stops working, but `cljs_repl` still works. Use it to detect a crash via `(some? (:exception @app.main.store/state))`.
|
||||
For details on handling crashes, read memory `frontend/handling-crashes`.
|
||||
For details on handling crashes, read memory `mem:frontend/handling-crashes`.
|
||||
|
||||
33
.serena/memories/exporter/core.md
Normal file
33
.serena/memories/exporter/core.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Exporter Architecture and Workflow
|
||||
|
||||
`exporter/`: CLJS/Node headless export service. Depends on `common/`; uses Playwright plus export JS/CLJS deps for SVG/PDF/assets.
|
||||
|
||||
## Layout and commands
|
||||
|
||||
- Source: `exporter/src/`; config: `deps.edn`, `shadow-cljs.edn`, `package.json`; runtime helpers/assets: `vendor/`, `scripts/`.
|
||||
- From `exporter/`: setup `./scripts/setup`; watch `pnpm run watch` or `pnpm run watch:app`; production build `pnpm run build`; lint `pnpm run lint`; format check/fix `pnpm run check-fmt` / `pnpm run fmt`.
|
||||
- Because exporter consumes `common/`, shared file/shape/model changes may need exporter verification even when the immediate change is not under `exporter/`.
|
||||
|
||||
## HTTP and browser pool
|
||||
|
||||
- POST body limit is about 60 MB. Exporter supports `application/transit+json`; request params merge query params and body params.
|
||||
- Map response bodies are Transit JSON and force HTTP 200; nil 200 bodies become 204.
|
||||
- Auth token comes from cookie `auth-token`, then uploads use Bearer auth plus the management shared key.
|
||||
- Each export job gets a fresh Playwright browser context. On success, the context closes and the browser returns to the pool; on error, the browser is destroyed instead of reused.
|
||||
- Borrow validates browser connection. Pool acquire timeout is about 10s; font loading timeout logs a warning and continues after about 15s.
|
||||
|
||||
## Export batching and async behavior
|
||||
|
||||
- `prepare-exports` groups entries by `[scale type]` and partitions groups into chunks of 50. Each partition uses file/page/share/name from its first item, so be careful if entries might cross those boundaries.
|
||||
- Single-export response is used only when multiple export is not forced and there is exactly one prepared export containing exactly one object.
|
||||
- Multi-object export can run async: when `wait` is false it returns a resource immediately and publishes progress/end/error to Redis by profile topic; when `wait` is true it waits for upload and returns the uploaded resource.
|
||||
- Frame export returns a resource immediately and publishes Redis updates; it does not follow the same `wait` option path.
|
||||
- ZIP entry names are sanitized and duplicates receive numeric suffixes.
|
||||
|
||||
## Render details
|
||||
|
||||
- Bitmap export differs for WASM vs non-WASM render paths: WASM forces Playwright `deviceScaleFactor` to 1 and passes scale through the render URL; non-WASM uses `deviceScaleFactor = scale`.
|
||||
- WebP is produced by taking a PNG screenshot and converting it with ImageMagick.
|
||||
- SVG export rasterizes text foreignObjects to PNG, converts through PPM/color masks/potrace, and reassembles SVG paths. It also replaces non-breaking spaces for SVG compatibility and drops empty defs/paths.
|
||||
- PDF export injects `@page` sizing through raw browser `evaluate` JavaScript; that code cannot rely on CLJS runtime helpers.
|
||||
- Temporary resources schedule local deletion, then uploads POST to `/api/management/methods/upload-tempfile` with `X-Shared-Key: exporter <management-key>` and Bearer auth.
|
||||
@ -1,76 +1,94 @@
|
||||
# ClojureScript REPL Access via shadow-cljs
|
||||
# ClojureScript REPL and Frontend Debugging
|
||||
|
||||
Execute code in the REPL via the Penpot MCP's `cljs_repl` tool.
|
||||
Execute code in the live frontend via the Penpot MCP `cljs_repl` tool. For browser-console debugging, the frontend also exports a `debug` JS namespace in development builds.
|
||||
|
||||
## Accessing App State
|
||||
## Accessing app state
|
||||
|
||||
The main store is `app.main.store/state`. It contains workspace metadata, selection, UI state, etc.
|
||||
However, **page objects are NOT in the main store atom**. They live behind derived refs.
|
||||
The main store is `app.main.store/state`. It contains workspace metadata, selection, UI state, profile, route, etc. Page objects are not under a `:workspace-data` key; use derived refs.
|
||||
|
||||
### Top-level store keys (subset)
|
||||
`:current-page-id`, `:current-file-id`, `:workspace-local`, `:workspace-global`,
|
||||
`:workspace-trimmed-page`, `:workspace-undo`, `:workspace-guides`, `:workspace-layout`,
|
||||
`:workspace-presence`, `:workspace-ready`, `:profile`, `:route`, etc.
|
||||
|
||||
**Notable absence:** There is no `:workspace-data` key in the store. The old path
|
||||
`(get-in state [:workspace-data :pages-index page-id :objects])` does NOT work.
|
||||
|
||||
### Getting page objects — use `app.main.refs/workspace-page-objects`
|
||||
```clojure
|
||||
;; This is a derived ref (reactive lens). Deref it directly:
|
||||
;; Current selection
|
||||
(mapv str (get-in @app.main.store/state [:workspace-local :selected]))
|
||||
|
||||
;; Current page objects
|
||||
(let [objects @app.main.refs/workspace-page-objects
|
||||
shape (get objects (parse-uuid "some-uuid-here"))]
|
||||
(select-keys shape [:name :type :x :y :width :height :fills :strokes :rotation :opacity :frame-id :parent-id]))
|
||||
```
|
||||
|
||||
### Getting the current selection
|
||||
Shape keys use kebab-case keywords. Internal `:rect` corresponds to "rectangle" in the JS Plugin API, and `:frame` corresponds to "board".
|
||||
|
||||
Component instance shapes carry `:component-id` and `:component-file` directly; `:component-root` flags the root of an instance. Use `app.common.types.container/get-head-shape` for nearest head and `get-instance-root` for outermost root; they differ for nested instances.
|
||||
|
||||
## Navigation recipe
|
||||
|
||||
To programmatically open a workspace file, all three ids are required:
|
||||
|
||||
```clojure
|
||||
;; Selection is in the main store under :workspace-local :selected
|
||||
(let [state @app.main.store/state
|
||||
selected (get-in state [:workspace-local :selected])]
|
||||
(mapv str selected))
|
||||
;; Returns vector of UUID strings for selected shapes
|
||||
(do (require '[app.main.data.common :as dcm])
|
||||
(app.main.store/emit! (dcm/go-to-workspace
|
||||
:team-id (parse-uuid "<team-id>")
|
||||
:file-id (parse-uuid "<file-id>")
|
||||
:page-id (parse-uuid "<page-id>"))))
|
||||
```
|
||||
|
||||
### Other useful store access
|
||||
Get `team-id` from `(:current-team-id @app.main.store/state)`. Get file ids from `(vals (:files @app.main.store/state))`. Get page ids by fetching file data, e.g. through `rp/cmd! :get-file` with current features.
|
||||
|
||||
## Reload the live runtime
|
||||
|
||||
`(.reload js/location)` (alias `app.util.dom/reload-current-window`) from `cljs_repl` reloads the browser page: clears `set!` runtime patches, re-fetches file state, and is the simplest crash recovery while the repl is live (`mem:frontend/handling-crashes`). To re-fetch only the current file's data without a full page reload, emit `(app.main.store/emit! (potok.v2.core/event :app.main.data.workspace/reload-current-file))`.
|
||||
|
||||
## Useful lookup helpers
|
||||
|
||||
`app.plugins.utils` contains state lookup helpers that are useful from any CLJS, despite living under `plugins/`:
|
||||
|
||||
- `locate-shape`, `locate-objects`, `locate-file`.
|
||||
- `locate-component` resolves through the outermost instance root.
|
||||
- `locate-head-component` resolves through the nearest component head.
|
||||
- `locate-library-component` does direct file-id/component-id lookup.
|
||||
|
||||
## Runtime patching with `set!`
|
||||
|
||||
Some frontend vars are deliberately mutable escape hatches for runtime instrumentation or circular-dependency patching. From `cljs_repl`, use `set!` for temporary debugging of CLJS vars such as `app.main.store/on-event`, `app.main.errors/reload-file`, `app.main.errors/is-plugin-error?`, `app.main.errors/last-report`, or `app.main.errors/last-exception`. These patches affect only the live browser runtime and disappear on reload or recompilation.
|
||||
|
||||
```clojure
|
||||
;; Current page id
|
||||
(:current-page-id @app.main.store/state)
|
||||
|
||||
;; Verify state is accessible
|
||||
(some? @app.main.store/state) ;; should be true
|
||||
|
||||
;; workspace-local keys: :zoom :selected :hide-toolbar :last-selected :vbox
|
||||
;; :highlighted :vport :expanded :selrect :zoom-inverse
|
||||
;; Log non-noisy Potok events temporarily.
|
||||
(set! app.main.store/on-event
|
||||
(fn [event]
|
||||
(when (potok.v2.core/event? event)
|
||||
(.log js/console (potok.v2.core/repr-event event)))))
|
||||
```
|
||||
|
||||
### Shape data structure (internal ClojureScript representation)
|
||||
Shape keys use kebab-case keywords (`:fill-color`, `:fill-opacity`, `:parent-id`, `:frame-id`).
|
||||
The shape `:type` is a keyword like `:rect`, `:path`, `:text`, `:ellipse`, `:image`, `:bool`, `:svg-raw`, `:frame`, `:group`.
|
||||
Note `:rect` in CLJS corresponds to "rectangle" in the JS Plugin API, and `:frame` corresponds to "board".
|
||||
Restore mutable hooks after debugging, or reload the frontend. Use JVM `alter-var-root` only for JVM Clojure; it is not the normal way to patch live CLJS browser vars.
|
||||
|
||||
Component instance shapes additionally carry `:component-id` and `:component-file` directly, and `:component-root` flags the root of an instance. To navigate from a shape to its component, use `app.common.types.container/get-head-shape` (nearest head) or `get-instance-root` (outermost root) — these differ when instances are nested.
|
||||
## Browser-console debug namespace
|
||||
|
||||
### Helper utilities (`app.plugins.utils`)
|
||||
Despite living under `plugins/`, these are general-purpose lookup helpers usable from any CLJS:
|
||||
- `locate-shape` — find a shape by file-id, page-id, id
|
||||
- `locate-objects` — get the object tree for a page
|
||||
- `locate-component` — resolve the component for a shape (walks to **outermost** instance root, not nearest head — beware when instances are nested)
|
||||
- `locate-library-component` — direct lookup by file-id and component-id
|
||||
- `locate-file` — look up a file by id from state
|
||||
In development, the JS console exposes `debug` helpers from `frontend/src/debug.cljs`:
|
||||
|
||||
## Notes
|
||||
- The `:main` build has multiple modules: shared, main, main-workspace, rasterizer, etc.
|
||||
- `app.main.store/state` is a potok store (wrapping an okulary atom) created via `defonce`
|
||||
- Use `timeout` to avoid hanging if the browser is disconnected
|
||||
```javascript
|
||||
debug.set_logging("namespace", "debug");
|
||||
debug.dump_state();
|
||||
debug.dump_buffer();
|
||||
debug.get_state(":workspace-local :selected");
|
||||
debug.dump_objects();
|
||||
debug.dump_object("Rect-1");
|
||||
debug.dump_selected();
|
||||
debug.dump_tree(true, true);
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
Visual workspace debug overlays can be toggled with `debug.toggle_debug("bounding-boxes")`, `"group"`, `"events"`, or `"rotation-handler"`; `debug.debug_all()` and `debug.debug_none()` toggle all visual aids.
|
||||
|
||||
`cljs_repl` may not connect to the right runtime when several are attached (e.g. workspace tab + rasterizer). Verify with `(.-title js/document)` — it should show your file name, not "Penpot - Rasterizer".
|
||||
For temporary source traces, prefer existing logging (`app.common.logging` / `app.util.logging`) or short-lived `prn`, `app.common.pprint/pprint`, `js/console.log`, or `js-debugger` calls. Remove temporary source instrumentation before committing.
|
||||
|
||||
To list runtimes or target one by client-id, use `npx shadow-cljs clj-eval` from `/home/penpot/penpot/frontend`. It talks to the shadow-cljs JVM process, so unlike `cljs_repl` it has access to `shadow.cljs.devtools.api`:
|
||||
## Runtime targeting
|
||||
|
||||
`cljs_repl` may connect to the wrong runtime when several are attached, such as workspace plus rasterizer. Verify with `(.-title js/document)`; it should show the workspace file name, not "Penpot - Rasterizer".
|
||||
|
||||
To list or target shadow-cljs runtimes, run from `/home/penpot/penpot/frontend`:
|
||||
|
||||
```bash
|
||||
printf '(shadow.cljs.devtools.api/repl-runtimes :main)\n' | timeout 10 npx shadow-cljs clj-eval --stdin
|
||||
printf '(shadow.cljs.devtools.api/cljs-eval :main "<cljs-code>" {:client-id 5})\n' | timeout 10 npx shadow-cljs clj-eval --stdin
|
||||
```
|
||||
```
|
||||
|
||||
Use command timeouts so a disconnected browser does not hang the session.
|
||||
22
.serena/memories/frontend/compile-diagnostics.md
Normal file
22
.serena/memories/frontend/compile-diagnostics.md
Normal file
@ -0,0 +1,22 @@
|
||||
# Frontend Compile Diagnostics
|
||||
|
||||
Separate from runtime crash recovery.
|
||||
|
||||
## First check the shadow-cljs build
|
||||
|
||||
Use the Penpot MCP `cljs_compiler_output` tool to inspect the latest shadow-cljs `:main` build status. This is the fastest way to distinguish a bad build from a runtime error in the browser.
|
||||
|
||||
Recommended order after CLJ/CLJC/CLJS source edits:
|
||||
1. Run `cljs_compiler_output`.
|
||||
2. If the compiler reports a Clojure syntax problem, especially unmatched delimiters or a confusing location, run `clj_check_parentheses` on the absolute path of the suspect `.clj`, `.cljc`, or `.cljs` file.
|
||||
3. After the build is healthy, use `mem:frontend/cljs-repl`, browser tools, or runtime crash checks for behavior.
|
||||
|
||||
## Parentheses checker
|
||||
|
||||
`clj_check_parentheses` analyzes one Clojure/ClojureScript source file and reports the area likely responsible for unclosed parentheses/brackets/braces. Use it when compiler output points near EOF, points at a misleading later form, or says delimiter-related syntax errors.
|
||||
|
||||
## Hot reload notes
|
||||
|
||||
When the frontend shadow-cljs watch process is running, edits to CLJC files in `common/` are normally recompiled into the browser automatically. Do not restart the frontend before checking `cljs_compiler_output`; stale behavior is often a failed build.
|
||||
|
||||
For production/minified stack traces, build the production bundle from `frontend/` with `pnpm run build:app`. Output and source maps are generated under `frontend/resources/public/js`; inspect source maps or shadow-cljs reports using build ids from `shadow-cljs.edn`.
|
||||
42
.serena/memories/frontend/core.md
Normal file
42
.serena/memories/frontend/core.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Frontend Architecture and Workflow
|
||||
|
||||
Frontend: CLJS SPA; React/Rumext; Potok; RxJS; okulary refs; SCSS modules; shared `common/`; JS/TS workspace packages.
|
||||
|
||||
## Stable namespace map
|
||||
|
||||
- `app.main.ui.*`: Rumext/React UI components for workspace, dashboard, viewer, settings, auth, nitrate, etc.
|
||||
- `app.main.data.*`: Potok event handlers and side effects.
|
||||
- `app.main.refs`: reactive refs/lenses over store and derived workspace data.
|
||||
- `app.main.store`: Potok store and `emit!`.
|
||||
- `app.plugins.*` and `app.plugins`: CLJS implementation of Plugin JS API proxies.
|
||||
- `app.render_wasm.*`: frontend bridge to Rust/WASM renderer.
|
||||
- `app.util.*`: DOM, HTTP, i18n, keyboard, codegen, and general frontend utilities.
|
||||
- `frontend/packages/*` and `frontend/text-editor`: JS/TS workspace packages consumed by the app.
|
||||
- Nitrate subscription/organization UI and flows live under `app.main.data.nitrate` and `app.main.ui.nitrate*`; backend/API behavior is covered by backend memories, and shared permission rules are in `common/src/app/common/types/nitrate_permissions.cljc`.
|
||||
|
||||
## Focused memory routing
|
||||
|
||||
UI and packages:
|
||||
- App UI components, SCSS modules, style-system boundaries, accessibility, i18n, and render performance: `mem:frontend/ui-conventions-and-style-system`.
|
||||
- JS/TS packages, shared UI package, text editor, Storybook, and package builds: `mem:frontend/ui-packages-text-editor-workflow`.
|
||||
|
||||
Workspace behavior:
|
||||
- Workspace state, commits, persistence, undo, repo calls, and refs: `mem:frontend/workspace-state-persistence-subtleties`.
|
||||
- Workspace transforms, modifier previews, WASM modifier integration, and transform commits: `mem:frontend/workspace-transform-subtleties`.
|
||||
- Workspace token application/propagation: `mem:frontend/workspace-token-subtleties`; shared token data/schema: `mem:common/tokens-schema-subtleties`.
|
||||
|
||||
App shell and product flows:
|
||||
- Routing, root app shell, websocket, and global errors: `mem:frontend/routing-app-shell-subtleties`.
|
||||
- Dashboard and viewer flows: `mem:frontend/dashboard-viewer-subtleties`.
|
||||
- Plugin JS API runtime inside the frontend app: `mem:frontend/plugin-api-to-cljs-binding`.
|
||||
|
||||
Diagnostics and validation:
|
||||
- Runtime inspection and navigation: `mem:frontend/cljs-repl`.
|
||||
- Source-edit compile/hot-reload diagnostics: `mem:frontend/compile-diagnostics`.
|
||||
- Runtime crash recovery: `mem:frontend/handling-crashes`.
|
||||
- Tests, lint, format, and live verification: `mem:frontend/testing`.
|
||||
- Real pointer/keyboard gesture reproduction: `mem:frontend/playwright-gestures`.
|
||||
|
||||
## Areas without focused memories
|
||||
|
||||
These frontend areas currently have no dedicated Serena memory beyond this architecture entry and nearby source/tests: clipboard, drawing tools, boolean/path operations, interactions/prototyping, color/style asset management, grid-layout editing UI, comments UI, fonts UI, and many dashboard/settings subflows. Treat work there as less memory-covered and inspect source/tests more carefully.
|
||||
16
.serena/memories/frontend/dashboard-viewer-subtleties.md
Normal file
16
.serena/memories/frontend/dashboard-viewer-subtleties.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Frontend Dashboard and Viewer Subtleties
|
||||
|
||||
## Dashboard
|
||||
|
||||
- Dashboard initialization fetches projects and fonts for the team, then listens to websocket messages only for global topic `uuid/zero` or the current profile id.
|
||||
- Project fetch replaces each project map completely instead of merging, so fields such as `deleted-at` can disappear cleanly.
|
||||
- Dashboard file/project mutations are often optimistic local updates with fire-and-forget RPC watchers. Bulk permanent delete/restore paths use SSE progress and progress notifications.
|
||||
- File creation/duplication strips file `:data` before putting file summaries into dashboard state.
|
||||
|
||||
## Viewer
|
||||
|
||||
- Viewer initialization sets `:current-file-id`, `:current-share-id`, and `:viewer-local`, then fetches the view-only bundle. Comment threads are fetched only for logged-in users.
|
||||
- Viewer bundle fetch sends the full supported feature set because anonymous shared viewers may not know team-enabled features.
|
||||
- View-only bundles can contain pointer values in `:pages-index` and file data. Viewer resolves those fragments with `:get-file-fragment` before storing the bundle.
|
||||
- `bundle-fetched` indexes pages and precomputes viewer frames/all-frames, stores libraries/users/thumbnails/permissions under `:viewer`, then navigates to frame id, query index, or auto-selected frame.
|
||||
- Viewer zoom and interaction mode changes update both `:viewer-local` and the `:viewer` route query params.
|
||||
@ -1,41 +1,38 @@
|
||||
# Handling Penpot Frontend Crashes
|
||||
# Frontend Runtime Crash Handling
|
||||
|
||||
When the Penpot frontend crashes, it usually shows the **Internal Error** page (title text "Something bad happened", class `main_ui_static__download-link`).
|
||||
## Detect a runtime workspace crash
|
||||
|
||||
A typical error pattern is: Changes go through (JS API, `execute_code`), but about 1-2s later, an `update-file` request hits the backend with the change and gets rejected.
|
||||
So be sure to check the status for a crash.
|
||||
Runtime crashes usually show the Internal Error page with title text "Something bad happened" and class `main_ui_static__download-link`. A common pattern is: changes go through via JS API / `execute_code`, then 1-2s later an `update-file` request reaches the backend and is rejected.
|
||||
|
||||
After a crash, `execute_code` is unusable (no instances connected), and any data in `storage` is lost, but `cljs_repl` keeps working!
|
||||
After a crash, `execute_code` can become unusable because no plugin instances are connected and any data in its `storage` is lost, but `cljs_repl` usually still works.
|
||||
|
||||
## 1. Detect the crash
|
||||
Check crash state:
|
||||
|
||||
cljs REPL `(some? (:exception @app.main.store/state))` returns `true` when the Internal Error page is showing,
|
||||
`false` on a healthy workspace (and after a successful reload).
|
||||
```clojure
|
||||
(some? (:exception @app.main.store/state))
|
||||
```
|
||||
|
||||
## 2. Read the cause
|
||||
It returns `true` when the Internal Error page is showing and `false` on a healthy workspace or after a successful reload.
|
||||
|
||||
## Read the runtime cause
|
||||
|
||||
The exception is stored at `(:exception @app.main.store/state)`. Useful keys:
|
||||
|
||||
- `:type`, `:code`, `:status` — error class (e.g. `:validation` / `:referential-integrity` / `400`)
|
||||
- `:hint`, `:details` — human-readable explanation; `:details` typically contains a vector of validation problems with `:shape-id`, `:page-id`, `:args`, etc.
|
||||
- `:uri` — the API endpoint that returned the error (e.g. `update-file`)
|
||||
- `:app.main.errors/instance` — the underlying JS Error object
|
||||
- `:app.main.errors/trace` — JS stack trace string (only shows the response-handling path, not the dispatch site that produced the bad change)
|
||||
- `:type`, `:code`, `:status`: error class, e.g. `:validation`, `:referential-integrity`, `400`.
|
||||
- `:hint`, `:details`: human-readable explanation; `:details` often contains validation problems with `:shape-id`, `:page-id`, `:args`, etc.
|
||||
- `:uri`: API endpoint that returned the error, e.g. `update-file`.
|
||||
- `:app.main.errors/instance`: underlying JS Error object.
|
||||
- `:app.main.errors/trace`: JS stack trace string, usually response-handling path rather than the dispatch site that produced the bad change.
|
||||
|
||||
```
|
||||
```clojure
|
||||
(let [ex (:exception @app.main.store/state)]
|
||||
(select-keys ex [:type :code :status :hint :details :uri]))
|
||||
```
|
||||
|
||||
For backend validation errors (`:type :validation`), `:details` is the most informative field — it tells you exactly which shape and which invariant was violated.
|
||||
For backend validation errors (`:type :validation`), `:details` is usually the most informative field; it identifies the shape and invariant that failed.
|
||||
|
||||
## 3. Recover and continue testing
|
||||
## Recover and continue testing
|
||||
|
||||
Reload steps:
|
||||
1. List tabs with `playwright:browser_tabs` (`action: list`) and find the Penpot workspace tab (URL contains `/#/workspace`, title ends in `- Penpot`).
|
||||
2. If it isn't the current tab, select it via `playwright:browser_tabs` (`action: select`, `index: <n>`). The selected tab's URL then appears as "Page URL" in the result.
|
||||
3. Reload by calling `playwright:browser_navigate` with that same URL.
|
||||
4. Confirm recovery: `(some? (:exception @app.main.store/state))` should now return `false`.
|
||||
Simplest path when `cljs_repl` is still live (usually true after a crash): reload via repl with `(.reload js/location)` — see `mem:frontend/cljs-repl`. Alternatively via Playwright: find the workspace tab (URL contains `/#/workspace`, title ends `- Penpot`), select it if not current, then `playwright:browser_navigate` to that same URL. Either way, confirm recovery with `(some? (:exception @app.main.store/state))` returning `false`.
|
||||
|
||||
Whether the offending change persists depends on the crash type:
|
||||
For **backend-rejected changes** (e.g. `:type :validation`, 4xx on `update-file`), changes are NOT persisted. Reload restores the pre-crash state — safe to retry.
|
||||
For backend-rejected changes, such as validation errors on `update-file`, changes are not persisted. Reload restores the pre-crash state, so it is safe to retry after fixing the cause.
|
||||
@ -1,25 +0,0 @@
|
||||
# How the Plugin JS API connects to ClojureScript
|
||||
|
||||
## Type Definitions
|
||||
- `plugins/libs/plugin-types/index.d.ts` contains TypeScript type declarations (e.g. `ShapeBase`, `LibraryComponent`).
|
||||
- These are **type-only** — no runtime code. The actual objects are constructed in ClojureScript.
|
||||
|
||||
## Runtime Shape Proxy
|
||||
- `frontend/src/app/plugins/shape.cljs` builds the JS shape proxy via `obj/reify`.
|
||||
- Each method/property from the TS interface (e.g. `:component`, `:isComponentRoot`, `:componentHead`) is defined as a keyword entry in the `obj/reify` form, with a ClojureScript function as the implementation.
|
||||
- The proxy is created by the `shape-proxy` function, which takes `plugin-id`, `file-id`, `page-id`, and shape `id`, and closes over them.
|
||||
|
||||
## Library Proxies
|
||||
- `frontend/src/app/plugins/library.cljs` defines proxies for library types like `LibraryComponentProxy` (via `lib-component-proxy`), also using `obj/reify`.
|
||||
- The proxy satisfies the `LibraryComponent` TS interface, exposing `.id`, `.name`, `.path`, etc.
|
||||
|
||||
## Circular Dependency Resolution
|
||||
- `shape.cljs` and `library.cljs` have circular dependencies (shapes reference library component proxies and vice versa).
|
||||
- `shape.cljs` declares forward references as mutable `def nil` vars (e.g. `(def lib-component-proxy nil)`, line 144).
|
||||
- `frontend/src/app/plugins.cljs` patches them at load time: `(set! shape/lib-component-proxy library/lib-component-proxy)`.
|
||||
- Same pattern for `lib-typography-proxy?` and `variant-proxy`.
|
||||
|
||||
## Key Domain Namespaces
|
||||
- `app.common.types.component` (aliased `ctk`) — component predicates: `instance-root?`, `instance-head?`, `in-component-copy?`, `is-variant?`
|
||||
- `app.common.types.container` (aliased `ctn`) — container/tree operations: `in-any-component?`, `get-instance-root`, `get-head-shape`, `inside-component-main?`
|
||||
- `app.common.types.file` (aliased `ctf`) — file-level operations: `resolve-component`, `get-ref-shape`
|
||||
@ -1,14 +0,0 @@
|
||||
# Navigating to a File in the Workspace
|
||||
|
||||
To programmatically open a file in the workspace, use `cljs_repl` with:
|
||||
```clojure
|
||||
(do (require '[app.main.data.common :as dcm])
|
||||
(app.main.store/emit! (dcm/go-to-workspace
|
||||
:team-id (parse-uuid "<team-id>")
|
||||
:file-id (parse-uuid "<file-id>")
|
||||
:page-id (parse-uuid "<page-id>"))))
|
||||
```
|
||||
**All three IDs are required.** You can get:
|
||||
- `team-id` from `(:current-team-id @app.main.store/state)`
|
||||
- `file-id` from the dashboard files: `(vals (:files @app.main.store/state))`
|
||||
- `page-id` by fetching the file: `(get-in file-data [:data :pages])` via `(rp/cmd! :get-file {:id file-id :features (get @app.main.store/state :features)})`
|
||||
117
.serena/memories/frontend/penpot-to-browser-coords.md
Normal file
117
.serena/memories/frontend/penpot-to-browser-coords.md
Normal file
@ -0,0 +1,117 @@
|
||||
# Penpot Canvas → Playwright Viewport Coordinate Mapping
|
||||
|
||||
## Goal
|
||||
Map Penpot shape coordinates (from the JS/ClojureScript API) to browser viewport CSS pixels
|
||||
so that Playwright mouse actions (click, drag, hover) can target specific canvas objects.
|
||||
|
||||
## Key Facts
|
||||
|
||||
### Playwright coordinate system
|
||||
Playwright mouse coordinates are **viewport CSS pixels**: `(0, 0)` is the top-left of the
|
||||
browser's rendered content area (not the screen, not the OS window chrome).
|
||||
`getBoundingClientRect()` returns the same coordinate system — they are directly compatible.
|
||||
|
||||
### Canvas element location
|
||||
The Penpot canvas is rendered by two co-located elements:
|
||||
- `<canvas>` — the rasterised render
|
||||
- `<svg id="render">` — vector overlay
|
||||
- `<svg class="...viewport-controls ...">` — interaction/control layer (has the `viewBox`)
|
||||
|
||||
Get the canvas origin with:
|
||||
```js
|
||||
document.querySelector("#render").getBoundingClientRect()
|
||||
// => { left: 318, top: 0, width: 514, height: 586, ... } (values vary with window size/panels)
|
||||
```
|
||||
The left offset (currently ~318 px) is caused by the left-side panel (layers, assets).
|
||||
|
||||
### Zoom and pan state
|
||||
Available in two equivalent ways:
|
||||
|
||||
**1. App state (ClojureScript):**
|
||||
```clojure
|
||||
(let [wl (get @app.main.store/state :workspace-local)]
|
||||
{:zoom (get wl :zoom) ; scale factor: penpot-units → CSS pixels
|
||||
:vbox (get wl :vbox)}) ; Rect {:x :y :width :height} — penpot coords of visible area
|
||||
```
|
||||
|
||||
**2. SVG viewBox attribute (DOM):**
|
||||
```js
|
||||
document.querySelector("svg.viewport-controls, [class*='viewport-controls']")
|
||||
.getAttribute("viewBox")
|
||||
// => "670 658.31 224 255.38" i.e. "vbox.x vbox.y vbox.width vbox.height"
|
||||
```
|
||||
Both sources are live and always in sync.
|
||||
|
||||
### Coordinate conversion formula
|
||||
```
|
||||
viewport_x = canvas_left + (penpot_x - vbox.x) * zoom
|
||||
viewport_y = canvas_top + (penpot_y - vbox.y) * zoom
|
||||
```
|
||||
|
||||
Sanity check: `vbox.width * zoom ≈ canvas CSS width` (and same for height). ✓
|
||||
|
||||
### Device Pixel Ratio
|
||||
The canvas physical pixel size = CSS size × DPR (observed DPR = 1.25, so canvas internal
|
||||
size 642×732 vs CSS size 514×586). This does **not** affect the formula — both
|
||||
`getBoundingClientRect()` and Playwright use CSS pixels.
|
||||
|
||||
### Ruler label offset
|
||||
The on-screen rulers show coordinates offset from absolute Penpot coordinates (they display
|
||||
frame-relative values, offset by ~the top-level frame's x/y). **Ignore for coordinate
|
||||
mapping** — use `vbox` directly.
|
||||
|
||||
---
|
||||
|
||||
## ClojureScript Helper (paste into cljs REPL session)
|
||||
|
||||
```clojure
|
||||
(defn penpot->viewport-coords
|
||||
"Convert Penpot canvas coordinates to browser viewport CSS pixel coordinates.
|
||||
Returns {:vp-x <number> :vp-y <number>} — pass directly to Playwright mouse actions."
|
||||
[penpot-x penpot-y]
|
||||
(let [state @app.main.store/state
|
||||
wl (get state :workspace-local)
|
||||
vbox (get wl :vbox)
|
||||
zoom (get wl :zoom)
|
||||
canvas (js/document.querySelector "svg.viewport-controls, #render")
|
||||
canvas-rect (.getBoundingClientRect canvas)]
|
||||
{:vp-x (+ (.-left canvas-rect) (* (- penpot-x (:x vbox)) zoom))
|
||||
:vp-y (+ (.-top canvas-rect) (* (- penpot-y (:y vbox)) zoom))}))
|
||||
```
|
||||
|
||||
Usage example — click the center of a shape:
|
||||
```clojure
|
||||
(let [shape (get-in @app.main.store/state [:files file-id :data :pages-index page-id :objects shape-id])
|
||||
cx (+ (:x shape) (/ (:width shape) 2))
|
||||
cy (+ (:y shape) (/ (:height shape) 2))
|
||||
{:keys [vp-x vp-y]} (penpot->viewport-coords cx cy)]
|
||||
;; pass vp-x, vp-y to Playwright page.mouse.click(vp-x, vp-y)
|
||||
{:vp-x vp-x :vp-y vp-y})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JavaScript equivalent (for use in Playwright scripts directly)
|
||||
|
||||
```js
|
||||
function penpotToViewport(penpotX, penpotY) {
|
||||
// Read viewBox from the controls SVG (always in sync with app state)
|
||||
const svg = document.querySelector('[class*="viewport-controls"]');
|
||||
const [vbX, vbY, vbW, vbH] = svg.getAttribute('viewBox').split(' ').map(Number);
|
||||
const rect = svg.getBoundingClientRect();
|
||||
const zoom = rect.width / vbW; // == rect.height / vbH
|
||||
return {
|
||||
x: rect.left + (penpotX - vbX) * zoom,
|
||||
y: rect.top + (penpotY - vbY) * zoom,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This function can be injected and called via `page.evaluate()` in Playwright:
|
||||
```js
|
||||
const {x, y} = await page.evaluate(
|
||||
([px, py]) => penpotToViewport(px, py),
|
||||
[penpotX, penpotY]
|
||||
);
|
||||
await page.mouse.click(x, y);
|
||||
```
|
||||
47
.serena/memories/frontend/playwright-gestures.md
Normal file
47
.serena/memories/frontend/playwright-gestures.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Driving Real User Gestures via Playwright
|
||||
|
||||
Use Playwright when the bug or behavior depends on Penpot's real input pipeline: pointer gestures, keyboard modifiers, drag/drop targeting, modifier propagation, hover/focus behavior, or alt-drag duplication. The plugin JS API and `penpot:execute_code` can bypass these paths by dispatching store/API operations directly.
|
||||
|
||||
## When `execute_code` Is Not Enough
|
||||
|
||||
`execute_code` runs in the plugin sandbox. It is excellent for creating shapes, calling Plugin API methods, and querying design data, but it does not faithfully reproduce all user gestures. If the issue involves interactive transforms, frame targeting during drop, drag previews, modifier keys, or canvas hit-testing, drive the browser with Playwright and inspect results via cljs-repl.
|
||||
|
||||
## Gesture Pattern
|
||||
|
||||
A reliable drag gesture generally needs:
|
||||
- focus on the canvas first;
|
||||
- key modifiers held from before mouse down until after mouse up;
|
||||
- intermediate mouse move events, not just start/end;
|
||||
- short waits so Penpot's drag pipeline observes the gesture;
|
||||
- a trailing wait for the transaction to commit.
|
||||
|
||||
Alt-drag duplication example:
|
||||
|
||||
```javascript
|
||||
async (page) => {
|
||||
await page.mouse.click(700, 700);
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const startX = 821, startY = 565, endX = 821, endY = 815;
|
||||
await page.keyboard.down('Alt');
|
||||
await page.mouse.move(startX, startY);
|
||||
await page.waitForTimeout(100);
|
||||
await page.mouse.down();
|
||||
await page.waitForTimeout(100);
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const t = i / 10;
|
||||
await page.mouse.move(startX + (endX - startX) * t,
|
||||
startY + (endY - startY) * t);
|
||||
await page.waitForTimeout(20);
|
||||
}
|
||||
await page.waitForTimeout(100);
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(100);
|
||||
await page.keyboard.up('Alt');
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
```
|
||||
|
||||
## Coordinate Planning
|
||||
|
||||
For reliably finding pixel positions of objects, see `mem:frontend/penpot-to-browser-coords`.
|
||||
34
.serena/memories/frontend/plugin-api-to-cljs-binding.md
Normal file
34
.serena/memories/frontend/plugin-api-to-cljs-binding.md
Normal file
@ -0,0 +1,34 @@
|
||||
# Frontend Plugin API Runtime Subtleties
|
||||
|
||||
## Type declarations vs runtime
|
||||
|
||||
- The Plugin API is a public facade over internal frontend/common data. Do not expect Plugin API property names, value shapes, or behavior boundaries to match internal CLJS attrs or helper APIs; inspect the relevant proxy and internal code path before using Plugin API observations in production internals or tests.
|
||||
- `plugins/libs/plugin-types/index.d.ts` contains TypeScript declarations only. Runtime objects are CLJS proxies built under `frontend/src/app/plugins/*.cljs` with `obj/reify`.
|
||||
- `shape.cljs` builds shape proxies with hidden ids and per-property CLJS implementations. `library.cljs` builds library proxies such as `LibraryComponentProxy`.
|
||||
- `shape.cljs`, `library.cljs`, and related namespaces break circular dependencies with mutable nil vars patched from `app.plugins` at load time. If a proxy constructor appears nil, check the patching path in `frontend/src/app/plugins.cljs`.
|
||||
|
||||
## Key Domain Namespaces
|
||||
- `app.common.types.component` (aliased `ctk`) — component predicates: `instance-root?`, `instance-head?`, `in-component-copy?`, `is-variant?`
|
||||
- `app.common.types.container` (aliased `ctn`) — container/tree operations: `in-any-component?`, `get-instance-root`, `get-head-shape`, `inside-component-main?`
|
||||
- `app.common.types.file` (aliased `ctf`) — file-level operations: `resolve-component`, `get-ref-shape`
|
||||
|
||||
## Runtime initialization and permissions
|
||||
|
||||
- The frontend initializes `@penpot/plugins-runtime` only after `features/initialize` and only when feature `plugins/runtime` is active. It also installs the runtime `isPluginError` predicate into frontend error handling.
|
||||
- Manifest parsing expands write permissions to read permissions (`content:write` => `content:read`, etc.). Permission checks also allow the all-zero plugin id and the hard-coded MCP plugin id.
|
||||
- Manifest URL origin differs by manifest version: v1 clears the path; v2 joins `.` to the plugin URL. Existing plugin ids are reused by matching manifest name and host.
|
||||
- The MCP plugin id is defined in `app.plugins.register` to avoid a circular dependency with workspace MCP code.
|
||||
|
||||
## Proxy behavior
|
||||
|
||||
- Public Plugin API objects are lightweight handles, not durable snapshots. Most getters locate fresh state from `app.main.store/state` using hidden `$id`, `$file`, `$page`, etc.
|
||||
- `not-valid` logs by default but throws when the plugin flag `throwValidationErrors` is enabled. The MCP execute-code handler deliberately enables that flag while running code.
|
||||
- `naturalChildOrdering` and `throwValidationErrors` are stored per plugin under `[:plugins :flags plugin-id ...]`; changing default behavior affects automation and MCP diagnostics.
|
||||
- Plugin data is stored under keyword namespaces: private data uses `(keyword "plugin" plugin-id)`, shared data uses `(keyword "shared" namespace)`.
|
||||
|
||||
## Events and history
|
||||
|
||||
- Plugin listeners are watches on the global store and callbacks are debounced about 10ms. Callback exceptions are caught and logged so plugin code does not crash the app.
|
||||
- `selectionchange` callbacks receive arrays of shape id strings, while `filechange`, `pagechange`, and `shapechange` return proxies.
|
||||
- `contentsave` fires only when persistence status transitions to `:saved`; it calls the callback with no value.
|
||||
- Plugin history `undoBlockBegin` creates a workspace undo transaction with a JS `Symbol`; `undoBlockFinish` commits that symbol. Missing finish eventually relies on the workspace transaction timeout.
|
||||
17
.serena/memories/frontend/routing-app-shell-subtleties.md
Normal file
17
.serena/memories/frontend/routing-app-shell-subtleties.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Frontend Routing, App Shell, Websocket, and Error Subtleties
|
||||
|
||||
## Router, app shell, and errors
|
||||
|
||||
- Routing uses browser-history hash tokens, but `on-navigate` rejects navigation if the current origin/path does not match `cf/public-uri`.
|
||||
- Route params are split into `:path` and `:query`; duplicate query params can become vectors, so use `rt/get-query-param` when a scalar is required.
|
||||
- Unknown/empty routes trigger an extra `get-profile`/`get-teams` check before redirecting. This avoids invitation and root-route race conditions.
|
||||
- The root app renders an exception page from `:exception` state before the normal error boundary. `rt/navigated` clears `:exception`.
|
||||
- Frontend error handling treats stale cross-build JS chunk failures specially: messages containing `$cljs$cst$` or `$cljs$core$I` plus undefined/null/not-a-function signatures trigger throttled reload.
|
||||
- Plugin-originated uncaught errors are identified through the plugin runtime hook and logged rather than turning into the global exception page.
|
||||
|
||||
## Store and websocket
|
||||
|
||||
For general store mechanics such as `emit!`, `last-events`, persistence, and undo, read `mem:frontend/workspace-state-persistence-subtleties`.
|
||||
|
||||
- Websocket initialization uses `cf/public-uri` joined with `ws/notifications`, converting `http/https` to `ws/wss`, and includes the current `session-id` as query param.
|
||||
- Reinitializing or finalizing websocket stops the previous receive stream. Incoming websocket payloads become Potok data events under `app.main.data.websocket/message`.
|
||||
40
.serena/memories/frontend/testing.md
Normal file
40
.serena/memories/frontend/testing.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Frontend Testing and Live Verification
|
||||
|
||||
Frontend validation: CLJS + React/Rumext + RxJS/Potok; SCSS modules; shared CLJC from `common/`.
|
||||
|
||||
## Unit tests
|
||||
|
||||
Frontend unit tests live under `frontend/test/frontend_tests/` and use `cljs.test`. They should be deterministic, avoid DOM/UI integration where possible, and mock side effects such as RPC, storage, timers, or network access.
|
||||
|
||||
From `frontend/`:
|
||||
- Full unit test run: `pnpm run test`.
|
||||
- Focused unit tests: edit `test/frontend_tests/runner.cljs` to narrow the suite, then run `pnpm run test`.
|
||||
- Build test target only: `pnpm run build:test`.
|
||||
- Watch tests: `pnpm run watch:test`.
|
||||
|
||||
## Playwright integration tests
|
||||
|
||||
Do not add, modify, or run Playwright integration tests under `frontend/playwright` unless explicitly asked. When explicitly asked, use `pnpm run test:e2e` or `pnpm run test:e2e --grep "pattern"` from `frontend/`; ensure dependencies are installed through `./scripts/setup` if the environment is not prepared.
|
||||
|
||||
Integration tests fake backend behavior by intercepting network/websocket traffic, so every RPC or websocket the page needs must be mocked. Use existing Page Object Models:
|
||||
- `BasePage.mockRPC` intercepts RPC calls and already prefixes `/api/rpc/command/`; pass command names such as `get-profile`, not full URLs.
|
||||
- Workspace or other websocket-using pages should extend/use `BaseWebSocketPage`, initialize websocket mocks before each test, and mock `/ws/notifications` with the provided helpers.
|
||||
- Prefer common locators/actions in POMs; ad-hoc locators can stay in a single test.
|
||||
|
||||
Locator priority should follow user-facing semantics: `getByRole`, `getByLabel`, `getByPlaceholder`, `getByText`, then semantic alternatives such as alt/title, with `getByTestId` as the last resort. Name tests from the user's perspective and prefer positive, single-purpose assertions.
|
||||
|
||||
## Lint and format
|
||||
|
||||
From `frontend/`:
|
||||
- CLJ/CLJS lint: `pnpm run lint:clj`.
|
||||
- JS lint currently no-ops via `pnpm run lint:js`.
|
||||
- SCSS lint: `pnpm run lint:scss`.
|
||||
- Format checks: `pnpm run check-fmt:clj`, `pnpm run check-fmt:js`, `pnpm run check-fmt:scss`.
|
||||
- Format fix: `pnpm run fmt`, or targeted `fmt:clj` / `fmt:js` / `fmt:scss`.
|
||||
- Translation formatting after i18n edits: `pnpm run translations`.
|
||||
|
||||
## Live browser verification
|
||||
|
||||
Because CLJC compiles to both JVM and CLJS, JVM/common tests can miss frontend-only state caused by browser runtime, WASM modifier math, or real pointer events. Use `mem:frontend/cljs-repl` to inspect live app state and `mem:frontend/playwright-gestures` when real input is needed.
|
||||
|
||||
For stale hot reload or failed CLJ/CLJC/CLJS source builds, read `mem:frontend/compile-diagnostics`. For Internal Error pages or delayed runtime crashes after automation/API actions, read `mem:frontend/handling-crashes`. Translation `.po` changes are bundled into `index.html` and require a browser refresh.
|
||||
56
.serena/memories/frontend/ui-conventions-and-style-system.md
Normal file
56
.serena/memories/frontend/ui-conventions-and-style-system.md
Normal file
@ -0,0 +1,56 @@
|
||||
# Frontend UI Conventions and Style System
|
||||
|
||||
## CLJS app UI
|
||||
|
||||
- Main app components live under `frontend/src/app/main/ui*` and normally use Rumext `mf/defc` with a `*` suffix for component vars and `[:> component* props]` call sites.
|
||||
- Components should have clear ownership. Use `children` for normal composition; use slotted props only when separate owned regions are needed. Do not style or structurally manipulate child DOM that the component did not instantiate.
|
||||
- Accept and merge a `class` prop when callers reasonably need layout/positioning customization. Use `mf/spread-props` so Rumext prop transformations such as `:class` -> `className` still apply.
|
||||
- Avoid boolean prop names ending in `?`; they do not translate cleanly to JavaScript props. Use type hints such as `^boolean` where JS truthiness/semantics matter.
|
||||
- Split large components into smaller private components when useful; `::mf/private true` is the local convention for private Rumext components.
|
||||
|
||||
## Styling
|
||||
|
||||
- Co-located SCSS modules are preferred. Use `app.main.style/stl` helpers from CLJS and design-system SCSS tokens/mixins instead of legacy global selectors or high-specificity nesting.
|
||||
- Keep CSS specificity low. Avoid nested selectors unless they target elements the component owns; CSS Modules already prevent class-name collisions.
|
||||
- Prefer CSS logical properties for directional spacing/layout (`padding-inline-start`, etc.). Physical `width`/`height` are still acceptable where they are clearer.
|
||||
- Use named design-system variables/tokens for spacing, borders, fixed dimensions, colors, and typography. Avoid hardcoded px/rem values and deprecated `resources/styles/common/refactor/spacing.scss` variables.
|
||||
- Use component-local CSS custom properties for variants and theming instead of one-off Sass variables when a component has multiple visual states.
|
||||
- Prefer DS typography components (`heading*`, `text*`) and typography mixins instead of plain text wrappers or deprecated typography mixins.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Prefer semantic HTML first: anchors for navigation/download/email links, buttons for actions, correct heading levels, and keyboard-focusable controls.
|
||||
- If native elements cannot be used, apply appropriate ARIA roles/patterns. Follow WAI-ARIA APG patterns for standard widgets.
|
||||
- Icon-only controls need an accessible name via surrounding text, `aria-label`, `alt`, or equivalent. Decorative images/icons should be hidden from assistive tech.
|
||||
|
||||
## I18n
|
||||
|
||||
- Translations must be resolved during render or render-time memoization, not at namespace load time. For static option lists, memoize inside render so locale changes still update labels.
|
||||
- Translation files live in `frontend/translations/*.po`. Translation changes are bundled into `index.html`; refresh the browser after changing translations because there is no hot reload for translation strings.
|
||||
- Run `pnpm run translations` from `frontend/` after adding/updating translation text.
|
||||
- Adding a new supported locale requires updates in both `frontend/src/app/util/i18n.cljs` (`supported-locales`) and `frontend/scripts/_helpers.js` (`langs`).
|
||||
|
||||
## Performance
|
||||
|
||||
- Keep expensive derived data in refs, memoized selectors, or pure helpers. In hot render paths, prefer existing `app.common.data.macros` helpers where local code already uses them.
|
||||
- Avoid creating new callback functions/objects inside hot renders when a named function, memoized callback, data attribute, or precomputed JS props object works.
|
||||
- Destructure props/state values used repeatedly. Avoid repeated deref/property access in render loops.
|
||||
|
||||
## Shared React UI package
|
||||
|
||||
- `frontend/packages/ui` is the shared React/Vite package. It should remain framework-neutral relative to the CLJS app store; reusable primitives belong here only when they do not depend on Potok/Rumext app state.
|
||||
- Package styles are emitted through the package build and copied into `frontend/resources/public/css/ui.css`; stale shared styles are often a build artifact issue.
|
||||
- Storybook is the primary visual harness for shared UI/package behavior. Use `mem:frontend/ui-packages-text-editor-workflow` for package build/test commands.
|
||||
|
||||
## Choosing a location
|
||||
|
||||
- Put editor/dashboard/viewer workflow logic in CLJS app namespaces close to the owning feature.
|
||||
- Put reusable presentational React primitives in `frontend/packages/ui` when they can be consumed without Penpot app state.
|
||||
- Put CLJS design-system components under `frontend/src/app/main/ui/ds`; new DS components need implementation, CSS module, Storybook story, optional MDX docs, and export from `frontend/src/app/main/ui/ds.cljs` with a JavaScript-friendly name.
|
||||
- Put text editing internals in `frontend/text-editor` when the behavior belongs to the JS editor package; use `mem:common/text-subtleties` for shared text data-model behavior.
|
||||
|
||||
## Validation
|
||||
|
||||
- For CLJS app UI, use `mem:frontend/testing`, `mem:frontend/compile-diagnostics`, and live browser/REPL checks when behavior depends on store or canvas state.
|
||||
- For shared UI package changes, run the package build plus Storybook/component tests when relevant.
|
||||
- For text editor changes, run `frontend/text-editor` tests and refresh/copy WASM artifacts if render-wasm output is involved.
|
||||
@ -0,0 +1,34 @@
|
||||
# Frontend UI Packages and Text Editor Workflow
|
||||
|
||||
`frontend/packages/`, `frontend/text-editor/`, Storybook/component tests. Separate from CLJS app UI under `frontend/src/app/main/ui`.
|
||||
|
||||
## Package boundaries
|
||||
|
||||
- `frontend/packages/ui` builds `@penpot/ui`, a React/Vite library package. It exports ESM and type declarations from `dist/`; React and ReactDOM are peer dependencies and must stay external in the Vite library build.
|
||||
- The UI package build copies generated `dist/index.css` into `frontend/resources/public/css/ui.css`. If shared UI styles look stale in the app, rebuild the package or check this copy step before debugging CLJS style code.
|
||||
- `frontend/text-editor` builds `@penpot/text-editor` from `src/editor/TextEditor.js`. It is a Vite JS package, not CLJS, and has its own Vitest/browser-test setup.
|
||||
- The text editor consumes render-wasm artifacts copied from `frontend/resources/public/js` into `frontend/text-editor/src/wasm`. Use `pnpm run wasm:update` after rebuilding `render-wasm` if tests or local dev use stale WASM files.
|
||||
- Other packages under `frontend/packages/` such as `tokenscript`, `draft-js`, and `mousetrap` are workspace dependencies used by the frontend app; do not assume their runtime behavior lives in CLJS namespaces.
|
||||
|
||||
## Commands
|
||||
|
||||
From `frontend/`:
|
||||
- Build app-side JS package assets: `pnpm run build:app:libs`.
|
||||
- Watch app-side JS package assets: `pnpm run watch:app:libs`.
|
||||
- Storybook build: `pnpm run build:storybook`; local Storybook: `pnpm run watch:storybook`.
|
||||
- Storybook/component tests: `pnpm run test:storybook`.
|
||||
|
||||
From `frontend/packages/ui`:
|
||||
- Build library and CSS artifact: `pnpm run build`.
|
||||
- Watch library build: `pnpm run watch`.
|
||||
|
||||
From `frontend/text-editor`:
|
||||
- Local Vite dev: `pnpm run dev`.
|
||||
- Tests: `pnpm run test`; coverage: `pnpm run coverage`; browser watch: `pnpm run test:watch:e2e`.
|
||||
- Format check: `pnpm run fmt:js`.
|
||||
|
||||
## Validation notes
|
||||
|
||||
- Frontend root `check-fmt:js` covers stories, Playwright scripts, frontend scripts, and `text-editor/**/*.js`; it does not replace package-specific builds/tests.
|
||||
- Changes to shared UI package exports should be validated both in the package build and in the consuming app/Storybook path.
|
||||
- Changes that alter text rendering/editing can involve `frontend/text-editor`, `render-wasm`, CLJS text integration, and `mem:common/text-subtleties`; verify the runtime that actually owns the changed behavior.
|
||||
@ -0,0 +1,33 @@
|
||||
# Frontend Workspace State and Persistence Subtleties
|
||||
|
||||
## Store and interaction streams
|
||||
|
||||
- `app.main.store/state` is the Potok store; `emit!` always returns nil. Store errors flow through the mutable `on-error` atom.
|
||||
- `last-events` keeps a filtered rolling buffer of about 50 event type strings and commit hint origins. It intentionally omits noisy websocket/persistence/pointer events.
|
||||
- `ongoing-tasks` controls `window.onbeforeunload`: any non-empty set blocks tab unload.
|
||||
- `app.main.streams/wasm-modifiers` and `workspace-selrect` are behavior subjects used for high-frequency interactive preview state that bypasses normal store updates and lenses.
|
||||
- Keyboard modifier streams merge a window blur signal so stuck modifier-key state is cleared after focus loss.
|
||||
|
||||
## Repo calls
|
||||
|
||||
- `app.main.repo/send!` uses GET only when the RPC name starts with `get-`, when all params are query params, or for configured special cases. Only GET requests are retried.
|
||||
- GET retry is limited to transient `:network`, `:bad-gateway`, `:service-unavailable`, and `:offline` errors with exponential backoff. Mutations are not retried.
|
||||
- A server SSE response is only accepted when the command is configured `:stream?`; otherwise it raises an unexpected-response assertion.
|
||||
|
||||
## Commits, undo, persistence
|
||||
|
||||
- `commit-changes` refuses to create commits unless `:permissions :can-edit` is true. It captures file revn/vern, selected-before, features, tags, undo group, and translation flag into a `::commit` event.
|
||||
- Applying a remote commit first rolls back pending local commits, applies the remote changes, then replays pending local redo changes. Index updates are emitted for undo, remote redo, and replayed redo paths.
|
||||
- Local commits are independently consumed by undo, persistence, WASM model updates, thumbnail/library watchers, and text position-data recalculation.
|
||||
- Persistence buffers local commits: status becomes pending after about 200ms, commits are flushed after about 3s or `::force-persist`, and buffered commits are merged per file before `:update-file`.
|
||||
- Persistence sends revn as the max of the commit revn and locally tracked latest revn; remote commits update that revn tracker.
|
||||
- Persistence is skipped in version preview/read-only mode or without edit permission.
|
||||
- Undo transactions can stay open only temporarily; timed-out pending transactions are force-committed after about 20s. Undo entries are capped at 50.
|
||||
- Undo/redo are ignored while a normal editor/drawing interaction is active, except grid-layout edition handles undo through this path.
|
||||
- After local commits and when render-wasm is active, text shapes get derived `:position-data` recomputed in a separate commit tagged `#{:position-data}`; that tag is excluded from the position-data watcher to avoid loops.
|
||||
|
||||
## Refs
|
||||
|
||||
- `refs/libraries` is explicitly deprecated for performance; prefer derefing `refs/files` and memoizing `select-libraries` in components.
|
||||
- `refs/workspace-page-objects` uses `identical?` equality, so preserving object map identity matters for avoiding derived-ref churn.
|
||||
- Selected-shapes refs use a small `{objects selected}` wrapper with custom equality before running `process-selected`; avoid bypassing that pattern in hot UI paths.
|
||||
17
.serena/memories/frontend/workspace-token-subtleties.md
Normal file
17
.serena/memories/frontend/workspace-token-subtleties.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Frontend Workspace Token Subtleties
|
||||
|
||||
## Token refs and visibility
|
||||
|
||||
- Workspace token refs intentionally hide the internal hidden theme from theme trees/lists and expose active tokens through `get-tokens-in-active-sets`.
|
||||
- Token values stored on shapes are token names under `:applied-tokens`, not token ids. Renames/group renames must update those paths in common token logic.
|
||||
|
||||
## Token application
|
||||
|
||||
- Token application refuses to run while a text shape is in text-editing mode and shows a warning instead.
|
||||
- Applying a token writes token names into shape `:applied-tokens`, resolves active tokens through Style Dictionary or `tokenscript` depending on feature flags, updates concrete shape attrs, and wraps the operation in an undo transaction.
|
||||
- Applying composite typography removes atomic typography token attrs; applying atomic typography removes the composite typography token attr.
|
||||
- Spacing tokens have a special split path: layout containers receive gap/padding updates, while immediate children of layouts receive margin updates.
|
||||
|
||||
## Propagation
|
||||
|
||||
- Token propagation resolves active tokens, buffers many `update-shapes` commits, walks the current page first then the remaining pages, clears affected frame/component thumbnails, and drops `:position-data` for text shapes on non-current pages so it can be regenerated.
|
||||
20
.serena/memories/frontend/workspace-transform-subtleties.md
Normal file
20
.serena/memories/frontend/workspace-transform-subtleties.md
Normal file
@ -0,0 +1,20 @@
|
||||
# Frontend Workspace Transform Subtleties
|
||||
|
||||
## Preview vs committed transforms
|
||||
|
||||
- High-frequency previews use `app.main.streams/wasm-modifiers` and `workspace-selrect` behavior subjects instead of normal store commits; components consume them through refs that wrap plain atoms.
|
||||
- `apply-modifiers*` is the lower-level commit path once object/text modifiers are ready. It updates frame guides, frame comment threads, and then emits `update-shapes` with `:reg-objects? true`.
|
||||
- Transform commits restrict diff attrs to `transform-attrs` to avoid scanning unrelated shape attrs.
|
||||
- Text transforms may carry derived `:position-data`; `assoc-position-data` attaches it while preserving the original text shape context.
|
||||
|
||||
## Component-copy touched suppression
|
||||
|
||||
- `calculate-ignore-tree` walks modified shapes and descendants to decide per copy-shape `ignore-geometry?`.
|
||||
- `check-delta` compares a copy's relative position/rotation to its component root before and after transform. If relative movement is under about 1px and size/rotation are effectively unchanged, geometry touching is suppressed.
|
||||
- This logic is why pure translations of component copies can avoid marking every descendant as geometry-touched, while resizes/rotations still propagate touched state.
|
||||
|
||||
## WASM bridge details
|
||||
|
||||
- WASM modifier updates set plugin/local props with parsed geometry/structure modifiers rather than directly mutating file data.
|
||||
- The position-data recomputation watcher ignores commits tagged `:position-data`; keep that tag when adding derived position-data commits.
|
||||
- Rotation has separate WASM and non-WASM event paths. Check both when changing rotation modifier semantics.
|
||||
30
.serena/memories/library/core.md
Normal file
30
.serena/memories/library/core.md
Normal file
@ -0,0 +1,30 @@
|
||||
# Library Architecture and Workflow
|
||||
|
||||
`library/`: builds `@penpot/library`; JS-facing in-memory Penpot file builder and `.penpot` ZIP exporter. Separate from main app runtime.
|
||||
|
||||
## Layout and commands
|
||||
|
||||
- Source: `library/src/`; tests: `library/test/`; experimentation/docs: `playground/`, `docs/`; config: `shadow-cljs.edn`, `deps.edn`, `package.json`.
|
||||
- From `library/`: build `pnpm run build`; bundle helper `pnpm run build:bundle` or `./scripts/build`; tests `pnpm run test`; watch `pnpm run watch` / `pnpm run watch:test`; lint `pnpm run lint`; format check/fix `pnpm run check-fmt` / `pnpm run fmt`.
|
||||
- When changing file-format construction or export behavior in `common/`, consider whether `@penpot/library` should be tested because it constructs Penpot files outside the app UI.
|
||||
|
||||
## JS API and builder state
|
||||
|
||||
- The JS build context wraps an atom and implements `IDeref`; `getInternalState` exposes the CLJ state converted to JS.
|
||||
- Public methods decode JS objects through the JSON transformer before calling common builder functions. Exceptions become JS `BuilderError` objects with enumerable `cause` and an `explain` getter for Malli explain data.
|
||||
- `create-build-context` can store an optional `referer`, later written into the export manifest.
|
||||
- The builder is stateful: call `addFile` before `addPage`. `addPage` resets the frame/group stack to the root and clears page-local naming state when the page closes.
|
||||
- `addBoard` and `addGroup` push onto the parent stack; matching close calls pop it. `closeGroup` requires at least one child and recalculates group geometry. Masked groups use the first child as mask and copy its geometry.
|
||||
- `commit-shape` emits `:add-obj` with `:ignore-touched true`, using the current parent, frame, and page from the stack.
|
||||
- Layer names are uniqued per current page; duplicate names get generated suffixes.
|
||||
- `addBool` converts an existing group into a bool shape and updates style/content/geometry via `:mod-obj` operations rather than adding a new object.
|
||||
- Media blobs are stored separately from file-media metadata; `add-file-media` requires a `BlobWrapper`.
|
||||
|
||||
## Export package
|
||||
|
||||
- `.penpot` ZIPs include `manifest.json`, file/page/shape JSON, components/colors/typographies/tokens, media metadata, and media object blobs.
|
||||
- Path/bool shape `:content` is converted to vectors before JSON encoding.
|
||||
- File export intentionally includes only selected top-level attrs plus data options; color export removes `:file-id` and drops empty paths.
|
||||
- Manifest type is `penpot/export-files`, version 1, generated by `penpot-library/%version%`, with optional referer and file relations.
|
||||
- Export generation is sequential and lazy: delayed JSON/blob work is computed only as each zip entry is written, and the progress callback receives `{total,item,path}` after each entry.
|
||||
- The library has compatibility defaults for features/migrations in the common builder; do not assume it always exports with the newest app-default migrations/features.
|
||||
87
.serena/memories/mcp/core.md
Normal file
87
.serena/memories/mcp/core.md
Normal file
@ -0,0 +1,87 @@
|
||||
# Penpot MCP
|
||||
|
||||
This subproject provides an MCP server for Penpot integration.
|
||||
The MCP server communicates with a Penpot plugin via WebSockets, allowing
|
||||
the MCP server to send tasks to the plugin and receive results,
|
||||
enabling advanced AI-driven features in Penpot.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- Language: TypeScript
|
||||
- Runtime: Node.js
|
||||
- Framework: MCP SDK (@modelcontextprotocol/sdk)
|
||||
- Build Tool: TypeScript Compiler (tsc) + esbuild
|
||||
- Package Manager: pnpm
|
||||
|
||||
## General Principles
|
||||
|
||||
IMPORTANT: Use an idiomatic, object-oriented style.
|
||||
In particular, this implies that, for any non-trivial interfaces, you use interfaces that expect explicitly typed abstractions
|
||||
rather than mere functions (i.e. use the strategy pattern, for example).
|
||||
|
||||
Comments:
|
||||
When describing parameters, methods/functions and classes, you use a precise style, where the initial (elliptical) phrase
|
||||
clearly defines *what* it is. Any details then follow in subsequent sentences.
|
||||
|
||||
When describing what blocks of code do, you also use an elliptical style and start with a lower-case letter unless
|
||||
the comment is a lengthy explanation with at least two sentences (in which case you start with a capital letter, as is
|
||||
required for sentences).
|
||||
|
||||
## Project Structure (Excerpt)
|
||||
|
||||
```
|
||||
mcp/
|
||||
├── packages/common/ # Shared type definitions
|
||||
│ ├── src/
|
||||
│ │ ├── index.ts # exports for shared types
|
||||
│ │ └── types.ts # PluginTaskResult, request/response interfaces
|
||||
│ └── package.json # @penpot-mcp/common package
|
||||
├── packages/server/ # MCP server subproject
|
||||
│ ├── src/
|
||||
│ │ ├── index.ts # entry point
|
||||
│ │ ├── PenpotMcpServer.ts # MCP server implementation (connection handling, tool registration, etc.)
|
||||
│ │ ├── Tool.ts # base class for tools
|
||||
│ │ ├── PluginTask.ts # base class for plugin tasks
|
||||
│ │ ├── tasks/ # PluginTask implementations
|
||||
│ │ └── tools/ # Tool implementations
|
||||
| ├── data/ # contains resources, such as API info and prompts
|
||||
│ └── package.json
|
||||
├── packages/plugin/ # Penpot plugin subproject
|
||||
│ ├── src/
|
||||
│ │ ├── main.ts # handles communication
|
||||
│ │ └── plugin.ts # plugin implementation
|
||||
│ └── package.json # Includes @penpot-mcp/common dependency
|
||||
└── prepare-api-docs # Python project for the generation of API docs
|
||||
```
|
||||
|
||||
## Key Development Tasks
|
||||
|
||||
### Adjusting the Prompts
|
||||
|
||||
The system prompt file (aka Penpot High-Level Overview) is located in
|
||||
`packages/server/data/initial_instructions.md`.
|
||||
|
||||
### Adding a new Tool
|
||||
|
||||
1. Implement the tool class in `packages/server/src/tools/` following the `Tool` interface.
|
||||
IMPORTANT: Do not catch any exceptions in the `executeCore` method. Let them propagate to be handled centrally.
|
||||
2. Register the tool in `PenpotMcpServer`.
|
||||
|
||||
Tools can be associated with a `PluginTask` that is executed in the plugin.
|
||||
Many tools build on `ExecuteCodePluginTask`, as many operations can be reduced to code execution.
|
||||
|
||||
### Adding a new PluginTask
|
||||
|
||||
1. Implement the input data interface for the task in `packages/common/src/types.ts`.
|
||||
2. Implement the `PluginTask` class in `packages/server/src/tasks/`.
|
||||
3. Implement the corresponding task handler class in the plugin (`packages/plugin/src/task-handlers/`).
|
||||
* In the success case, call `task.sendSuccess`.
|
||||
* In the failure case, just throw an exception, which will be handled centrally!
|
||||
4. Register the task handler in `packages/plugin/src/plugin.ts` in the `taskHandlers` list.
|
||||
|
||||
## Dev Tooling
|
||||
|
||||
From the `mcp/` directory, run
|
||||
|
||||
* `pnpm run build` to test the build of all packages
|
||||
* `pnpm run fmt` to apply the auto-formatter
|
||||
33
.serena/memories/memory-maintenance.md
Normal file
33
.serena/memories/memory-maintenance.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Memory Maintenance
|
||||
|
||||
## Discovery Model
|
||||
|
||||
- Core principle: progressive discovery through references, building a graph of memories.
|
||||
- Initially, agents are provided with the list of all memories (names only).
|
||||
- Agents should read `mem:critical-info` as the top-level entry point (graph root).
|
||||
This memory should contain references to other memories covering major project domains.
|
||||
The referenced memories shall, in turn, shall contain references to even more specific memories, and so on.
|
||||
The depth of the graph shall depend on the project complexity.
|
||||
- Use topics/folders to group related memories in order to make the content structure explicit.
|
||||
Folders can mirror project structure (e.g. modules like frontend/backend) or topics like debugging, architecture, etc.
|
||||
- Memory references must use a mem: prefix inside backticks, e.g. `mem:frontend/core`.
|
||||
The surrounding text should clearly indicate when to read the memory/which content to expect.
|
||||
The text should provide more precise guidance than the memory name alone,
|
||||
i.e. avoid a reference like "frontend debugging and error handling: `mem:frontend/handling-errors-and-debugging` and instead make clear which concrete aspects are covered in the memory.
|
||||
- Memories themselves should not contain information about when to read them; this is the responsibility of the referring memory.
|
||||
|
||||
## Style
|
||||
|
||||
Dense agent notes, not prose docs. Prefer invariants, terse bullets.
|
||||
Avoid obvious context, rationale, and examples unless they prevent likely mistakes.
|
||||
Keep guidance durable and generalizable, not task-local.
|
||||
|
||||
## Add/update threshold
|
||||
|
||||
Add or update memories only with stable, non-obvious project conventions that avoid complex rediscovery in the future.
|
||||
Do not add: quick-read facts; generic language/framework knowledge; one-off task notes; volatile line-level details; behavior likely to change soon.
|
||||
|
||||
## Maintenance Actions
|
||||
|
||||
- Renaming memories: References are updated automatically if handled via Serena's memory rename tool.
|
||||
- Checking for stale memories (e.g. after deletion): Call `serena memories check` for a report.
|
||||
37
.serena/memories/plugins/core.md
Normal file
37
.serena/memories/plugins/core.md
Normal file
@ -0,0 +1,37 @@
|
||||
# Plugins Architecture and Workflow
|
||||
|
||||
`plugins/`: standalone TypeScript/pnpm workspace for Plugin API packages and sample plugins. Related to, distinct from, frontend CLJS Plugin API runtime.
|
||||
|
||||
## Layout
|
||||
|
||||
- `libs/plugin-types`: TypeScript declarations for the public Penpot Plugin API. Type-only package; runtime behavior is implemented elsewhere.
|
||||
- `libs/plugins-runtime`: runtime that loads plugins and exposes/generated API behavior to plugin code.
|
||||
- `libs/plugins-styles`: reusable styling package for plugins.
|
||||
- `apps/*-plugin`: sample/development plugins. `apps/e2e`: plugin e2e tests.
|
||||
|
||||
## Dev Workflow
|
||||
|
||||
- From `plugins/`: install `pnpm -r install`; runtime dev server `pnpm run start` or `pnpm run start:app:runtime`; sample plugin `pnpm run start:plugin:<name>`; build runtime `pnpm run build:runtime`; build plugins `pnpm run build:plugins`; lint `pnpm run lint`; format `pnpm run format:check` / `pnpm run format`; tests `pnpm run test`; e2e `pnpm run test:e2e`.
|
||||
- If a change affects public Plugin API types or runtime, update `plugins/CHANGELOG.md`. Prefix type/signature entries with `**plugin-types:**`; runtime behavior entries with `**plugin-runtime:**`.
|
||||
- JS Plugin API behavior inside Penpot app: `mem:frontend/plugin-api-to-cljs-binding`; TS declarations are not runtime code; many API objects are CLJS proxies in `frontend/src/app/plugins/*.cljs`.
|
||||
|
||||
## Sandbox and global cleanup
|
||||
|
||||
- The runtime uses SES compartments. Public API return values are passed through `ses.safeReturn` before crossing back to plugin code.
|
||||
- Plugin `fetch` is sanitized: credentials are omitted and Authorization is blanked. The exposed response only includes ok/status/statusText/url/text/json.
|
||||
- Timer callbacks are wrapped to mark plugin-originated errors, and timeout/interval IDs are tracked so plugin close can clear them.
|
||||
- Plugin-originated errors are tracked in a WeakMap instead of mutating error objects, because SES can freeze errors.
|
||||
- Closing a plugin removes public API keys from the compartment globalThis.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
- Loading a plugin closes existing non-background plugins and resets the runtime registry. Be careful around `allowBackground` semantics when changing load/close behavior.
|
||||
- If sandbox evaluation fails, the runtime marks the error as plugin-originated, closes the plugin, and rethrows.
|
||||
- `plugin-manager` removes event listeners, timers, intervals, and modal state on close, and marks the plugin destroyed. Listener callbacks check that flag because Penpot events can fire after close.
|
||||
|
||||
## Modal/UI behavior
|
||||
|
||||
- Modal URL preparation differs by manifest version: v1 uses query string parameters, v2 puts parameters in the URL hash.
|
||||
- `openModal` is idempotent for the same iframe source and avoids reopening when the target URL is already displayed.
|
||||
- Modal permissions are derived from manifest permissions (`allow:downloads`, `clipboard:read`, `clipboard:write`).
|
||||
- `resizeModal` clamps to at least 200x200 and at most the window minus margins, adjusting transform so the modal remains in the viewport.
|
||||
32
.serena/memories/render-wasm/core.md
Normal file
32
.serena/memories/render-wasm/core.md
Normal file
@ -0,0 +1,32 @@
|
||||
# render-wasm Architecture and Workflow
|
||||
|
||||
`render-wasm/`: Rust crate compiled to WebAssembly via Emscripten/Skia; frontend loads generated JS/WASM renderer. FFI/memory/tile behavior: `mem:render-wasm/ffi-rendering-subtleties`.
|
||||
|
||||
## Stable Architecture
|
||||
|
||||
- Exported functions live around `src/main.rs` / `src/wapi.rs` and are called from ClojureScript bridge namespaces under `frontend/src/app/render_wasm*`.
|
||||
- Updates are two-phase: ClojureScript calls exported setters to push shape data, then `render_frame()` performs Skia drawing.
|
||||
- Rendering is tile-based and shape data is stored separately from hierarchy.
|
||||
|
||||
## Source Areas
|
||||
|
||||
- `src/state*`: renderer state structures.
|
||||
- `src/render/` and `src/render.rs`: tile/surface render pipeline.
|
||||
- `src/shapes/` and `src/shapes.rs`: shape data and Skia drawing.
|
||||
- `src/wasm/`, `src/wasm.rs`, `src/mem.rs`: JS/WASM memory and interop helpers.
|
||||
- `src/math/` and `src/view.rs`: geometry and viewport helpers.
|
||||
|
||||
## Build Environment
|
||||
|
||||
`./build` sources `_build_env`, which sets the Emscripten paths and `EMCC_CFLAGS`. The WASM heap starts at 256 MB and uses geometric growth.
|
||||
|
||||
## Commands
|
||||
|
||||
From `render-wasm/`:
|
||||
- Build/copy frontend artifacts: `./build`.
|
||||
- Watch rebuild: `./watch`.
|
||||
- Rust tests: `./test` or `cargo test <name>`.
|
||||
- Lint: `./lint`.
|
||||
- Format check: `cargo fmt --check`.
|
||||
|
||||
Do not change exported WASM function signatures without updating the corresponding frontend bridge and verifying the frontend renderer path.
|
||||
25
.serena/memories/render-wasm/ffi-rendering-subtleties.md
Normal file
25
.serena/memories/render-wasm/ffi-rendering-subtleties.md
Normal file
@ -0,0 +1,25 @@
|
||||
# render-wasm FFI and Rendering Subtleties
|
||||
|
||||
## FFI state and errors
|
||||
|
||||
- The renderer uses one unsafe global `STATE`; the `with_state*` macros currently panic on invalid state pointer. Treat state pointer validity as critical, not recoverable.
|
||||
- `#[wasm_error]` clears the error code on entry. Recoverable errors set code `0x01`, critical errors/panics set `0x02`, free the byte buffer, then panic so the CLJS bridge can catch and inspect `_read_error_code`.
|
||||
- The frontend bridge maps `0x01` to `:non-blocking` and `0x02` to `:panic` in ex-data (`:type :wasm-error`). Check actual bridge code if changing names; older comments/docs may use different labels.
|
||||
- WASM byte transfer is a single global slot. A caller that receives a pointer result must read and free it before another byte payload is written; errors free the slot via `#[wasm_error]`.
|
||||
|
||||
## Shape pool and loading
|
||||
|
||||
- Shapes are UUID-indexed, and hierarchy/structure is tracked separately. `ShapesPool::get` may return a cached modified clone when modifiers, structure, scale-content, or bool handling apply; `get_raw` bypasses those derived values.
|
||||
- Bulk loading uses a `loading` flag. `touch_current` / `touch_shape` avoid tile invalidation while loading; text layouts and final view setup must happen after loading ends.
|
||||
- Many setters mutate only the current shape selected by `use_shape` / current-shape APIs. If no current shape is selected, some mutation blocks are skipped silently.
|
||||
- `set_parent_for_current_shape` only sets parent metadata and invalidates parent geometry; children must be updated separately to avoid duplicate children.
|
||||
- Child deletion marks descendants deleted and removes them from all indexed tiles, preserving undo/redo while avoiding stale pixels after panning.
|
||||
|
||||
## Tile/render behavior
|
||||
|
||||
- Interactive transforms are distinct from viewport fast mode. `set_modifiers_start` enables fast mode and interactive transform; interactive transform still flushes each animation frame.
|
||||
- During interactive transform, modifier tile invalidation is deferred to `render()` once per rAF. Outside interactive transform, `set_modifiers` rebuilds modifier tiles immediately.
|
||||
- `set_modifiers_end` disables fast/interactive state and cancels pending async render; the caller must request the final full-quality render.
|
||||
- Plain viewport fast mode (`options.is_viewport_interaction()`) renders from cache and does not flush target output inside `process_animation_frame`; interactive transforms do flush.
|
||||
- Zoom changes rebuild the tile index while preserving cached tile textures. Avoid replacing that path with shallow rebuilds if blur/shadow cache preservation matters.
|
||||
- Pending tile priority is intentionally reversed by pop order; check the queue construction before changing tile scheduling.
|
||||
32
.serena/memories/workflow/creating-commits.md
Normal file
32
.serena/memories/workflow/creating-commits.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Creating Commits
|
||||
|
||||
Commit only on explicit request. Before commit: `git status`; exclude unrelated user changes.
|
||||
|
||||
## Message Format
|
||||
|
||||
```
|
||||
:emoji: Subject line (imperative, capitalized, no period, <=70 chars)
|
||||
|
||||
Body explaining what changed and why.
|
||||
|
||||
Co-authored-by: <You (the LLM)>
|
||||
```
|
||||
|
||||
## Commit Type Emojis
|
||||
|
||||
`:bug:` bug fix · `:sparkles:` enhancement · `:tada:` new feature · `:recycle:` refactor · `:lipstick:` cosmetic · `:ambulance:` critical fix · `:books:` docs · `:construction:` WIP · `:boom:` breaking · `:wrench:` config · `:zap:` perf · `:whale:` docker · `:paperclip:` other · `:arrow_up:` dep upgrade · `:arrow_down:` dep downgrade · `:fire:` removal · `:globe_with_meridians:` translations · `:rocket:` epic/highlight
|
||||
|
||||
## Changelogs
|
||||
|
||||
For user-facing or notable changes, update the relevant changelog under the unreleased section:
|
||||
- Main app/modules (`backend`, `frontend`, `common`, `render-wasm`, `exporter`, `mcp`): root `CHANGES.md`.
|
||||
- Plugin subproject changes: `plugins/CHANGELOG.md`.
|
||||
|
||||
Entry format uses the matching category (`:sparkles:`, `:bug:`, etc.) and references the GitHub issue or Taiga story when available:
|
||||
|
||||
```
|
||||
- Description of change [Github #NNNN](https://github.com/penpot/penpot/issues/NNNN)
|
||||
- Description of change [Taiga #NNNN](https://tree.taiga.io/project/penpot/us/NNNN)
|
||||
```
|
||||
|
||||
Plugin API changelog prefixes: type/signature -> `**plugin-types:**`; runtime behavior -> `**plugin-runtime:**` in `plugins/CHANGELOG.md`.
|
||||
32
.serena/memories/workflow/creating-prs.md
Normal file
32
.serena/memories/workflow/creating-prs.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Creating Pull Requests
|
||||
|
||||
PR only on explicit request. Branch: issue/feature-specific; fallback `<type>/<short-description>` (`fix/...`, `feat/...`, `refactor/...`, `docs/...`, `chore/...`, `perf/...`).
|
||||
|
||||
## Title Format
|
||||
|
||||
PR titles follow commit title conventions:
|
||||
|
||||
```
|
||||
:emoji: Subject line (imperative, capitalized, no period, <=70 chars)
|
||||
```
|
||||
|
||||
See `mem:workflow/creating-commits` for emoji codes. Squash merge uses the PR title as the final commit subject, so title format matters.
|
||||
|
||||
## Description
|
||||
|
||||
Include concise sections covering:
|
||||
- what changed and why;
|
||||
- related GitHub issues or Taiga stories (`Fixes #NNNN`, `Relates to #NNNN`, `Taiga #NNNN`);
|
||||
- screenshots or recordings for UI-visible changes;
|
||||
- testing performed and residual risk;
|
||||
- breaking changes or migration notes, if any.
|
||||
|
||||
PR descriptions are expected to start with:
|
||||
|
||||
> **Note:** This PR was created with AI assistance as part of the Penpot MCP self-improvement initiative.
|
||||
|
||||
## Before Opening
|
||||
|
||||
- Follow `mem:workflow/creating-commits` for changelog expectations.
|
||||
- Run the focused tests/lints appropriate to touched modules.
|
||||
- Do not force-push during review unless the maintainer workflow explicitly asks for it.
|
||||
19
.serena/memories/workflow/docs.md
Normal file
19
.serena/memories/workflow/docs.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Docs Workflow
|
||||
|
||||
`docs/`: Penpot documentation site; Eleventy.
|
||||
|
||||
## Layout and Tooling
|
||||
|
||||
- `docs/package.json`: Eleventy commands and documentation-site dependencies.
|
||||
- `docs/README.md`: local setup and tooling notes.
|
||||
- Diagrams may use PlantUML, svgbob, mermaid, and C4/arc42 conventions depending on the existing page.
|
||||
|
||||
## Commands
|
||||
|
||||
From `docs/`:
|
||||
- Install deps if needed: `pnpm install`.
|
||||
- Local server: `pnpm start` or `pnpm run serve` (serves on `http://localhost:8080`).
|
||||
- Build: `pnpm run build`.
|
||||
- Watch: `pnpm run watch`.
|
||||
|
||||
Documentation changes should follow the existing page structure and rendered Help Center conventions rather than inventing a new style locally.
|
||||
@ -104,7 +104,8 @@ default_modes:
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: |
|
||||
CRITICAL: Always read the memory `critical-info` before you do anything else.
|
||||
CRITICAL: Read the memory `critical-info` IMMEDIATELY. It is absolutely critical for your work on this project - as is
|
||||
the Serena Instructions Manual. Do NOT wait for further instructions, read these critical items NOW!
|
||||
|
||||
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
||||
# such as docstrings or parameter information.
|
||||
|
||||
106
AGENTS.md
106
AGENTS.md
@ -1,104 +1,6 @@
|
||||
# AI Agent Guide
|
||||
CRITICAL: Read `.serena/memories/critical-info.md` NOW for important project information and instructions.
|
||||
|
||||
This document provides the core context and operating guidelines for AI agents
|
||||
working in this repository.
|
||||
In general, information about this project is stored in memories located in `.serena/memories/`.
|
||||
Memories reference other memories via `mem:name` (e.g. `mem:foo/bar` maps to `.serena/memories/foo/bar.md`).
|
||||
|
||||
## Before You Start
|
||||
|
||||
Before responding to any user request, you must:
|
||||
|
||||
1. Read this file completely.
|
||||
2. Identify which modules are affected by the task.
|
||||
3. Load the `AGENTS.md` file **only** for each affected module (see the
|
||||
architecture table below). Not all modules have an `AGENTS.md` — verify the
|
||||
file exists before attempting to read it.
|
||||
4. Do **not** load `AGENTS.md` files for unrelated modules.
|
||||
|
||||
## Role: Senior Software Engineer
|
||||
|
||||
You are a high-autonomy Senior Full-Stack Software Engineer. You have full
|
||||
permission to navigate the codebase, modify files, and execute commands to
|
||||
fulfill your tasks. Your goal is to solve complex technical tasks with high
|
||||
precision while maintaining a strong focus on maintainability and performance.
|
||||
|
||||
### Operational Guidelines
|
||||
|
||||
1. Before writing code, describe your plan. If the task is complex, break it
|
||||
down into atomic steps.
|
||||
2. Be concise and autonomous.
|
||||
3. Do **not** touch unrelated modules unless the task explicitly requires it.
|
||||
4. Commit only when explicitly asked. Follow the commit format rules in
|
||||
`CONTRIBUTING.md`.
|
||||
5. When searching code, prefer `ripgrep` (`rg`) over `grep` — it respects
|
||||
`.gitignore` by default.
|
||||
|
||||
## Changelogs
|
||||
|
||||
The project has two changelogs:
|
||||
|
||||
- **Main project changelog**: `CHANGES.md` (root of the repository). Tracks changes for the core Penpot application (backend, frontend, common, render-wasm, exporter, mcp).
|
||||
- **Plugins changelog**: `plugins/CHANGELOG.md`. Tracks changes for the plugins subproject only.
|
||||
|
||||
When making changes, add a changelog entry to the appropriate file under the
|
||||
`## <version> (Unreleased)` section in the correct category
|
||||
(`:sparkles: New features & Enhancements` or `:bug: Bugs fixed`).
|
||||
|
||||
## GitHub Operations
|
||||
|
||||
To obtain the list of repository members/collaborators:
|
||||
|
||||
```bash
|
||||
gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login'
|
||||
```
|
||||
|
||||
To obtain the list of open PRs authored by members:
|
||||
|
||||
```bash
|
||||
MEMBERS=$(gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login' | tr '\n' '|' | sed 's/|$//')
|
||||
gh pr list --state open --limit 200 --json author,title,number | jq -r --arg members "$MEMBERS" '
|
||||
($members | split("|")) as $m |
|
||||
.[] | select(.author.login as $a | $m | index($a)) |
|
||||
"\(.number)\t\(.author.login)\t\(.title)"
|
||||
'
|
||||
```
|
||||
|
||||
To obtain the list of open PRs from external contributors (non-members):
|
||||
|
||||
```bash
|
||||
MEMBERS=$(gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login' | tr '\n' '|' | sed 's/|$//')
|
||||
gh pr list --state open --limit 200 --json author,title,number | jq -r --arg members "$MEMBERS" '
|
||||
($members | split("|")) as $m |
|
||||
.[] | select(.author.login as $a | $m | index($a) | not) |
|
||||
"\(.number)\t\(.author.login)\t\(.title)"
|
||||
'
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Penpot is an open-source design tool composed of several modules:
|
||||
|
||||
| Directory | Language | Purpose | Has `AGENTS.md` |
|
||||
|-----------|----------|---------|:----------------:|
|
||||
| `frontend/` | ClojureScript + SCSS | Single-page React app (design editor) | Yes |
|
||||
| `backend/` | Clojure (JVM) | HTTP/RPC server, PostgreSQL, Redis | Yes |
|
||||
| `common/` | Cljc (shared Clojure/ClojureScript) | Data types, geometry, schemas, utilities | Yes |
|
||||
| `render-wasm/` | Rust -> WebAssembly | High-performance canvas renderer (Skia) | Yes |
|
||||
| `exporter/` | ClojureScript (Node.js) | Headless Playwright-based export (SVG/PDF) | No |
|
||||
| `mcp/` | TypeScript | Model Context Protocol integration | No |
|
||||
| `plugins/` | TypeScript | Plugin runtime and example plugins | No |
|
||||
|
||||
Some submodules use `pnpm` workspaces. The root `package.json` and
|
||||
`pnpm-lock.yaml` manage shared dependencies. Helper scripts live in `scripts/`.
|
||||
|
||||
### Module Dependency Graph
|
||||
|
||||
```
|
||||
frontend ──> common
|
||||
backend ──> common
|
||||
exporter ──> common
|
||||
frontend ──> render-wasm (loads compiled WASM)
|
||||
```
|
||||
|
||||
`common` is referenced as a local dependency (`{:local/root "../common"}`) by
|
||||
both `frontend` and `backend`. Changes to `common` can therefore affect multiple
|
||||
modules — test across consumers when modifying shared code.
|
||||
NOTE: When not using the Penpot agentic devenv, some tools mentioned in the memories will be unavailable.
|
||||
|
||||
@ -1,262 +0,0 @@
|
||||
# Penpot Backend – Agent Instructions
|
||||
|
||||
Clojure backend (RPC) service running on the JVM.
|
||||
|
||||
Uses Integrant for dependency injection, PostgreSQL for storage, and
|
||||
Redis for messaging/caching.
|
||||
|
||||
## General Guidelines
|
||||
|
||||
To ensure consistency across the Penpot JVM stack, all contributions must adhere
|
||||
to these criteria.
|
||||
|
||||
IMPORTANT: all CLI commands should be executed under backend/
|
||||
subdirectory for make them work correctly.
|
||||
|
||||
### 1. Testing & Validation
|
||||
|
||||
* **Coverage:** If code is added or modified in `src/`, corresponding
|
||||
tests in `test/backend_tests/` must be added or updated.
|
||||
|
||||
* **Execution:**
|
||||
* **Isolated:** Run `clojure -M:dev:test --focus backend-tests.my-ns-test` for the specific test namespace.
|
||||
* **Regression:** Run `clojure -M:dev:test` to ensure the suite passes without regressions in related functional areas.
|
||||
|
||||
### 2. Code Quality & Formatting
|
||||
|
||||
* **Linting:** All code must pass linter checks (run `pnpm run lint:clj` or `pnpm run lint` on the repository root)
|
||||
* **Formatting:** All the code must pass the formatting check (run `pnpm run
|
||||
check-fmt`). Use `pnpm run fmt` to fix formatting issues. Avoid "dirty"
|
||||
diffs caused by unrelated whitespace changes.
|
||||
* **Type Hinting:** Use explicit JVM type hints (e.g., `^String`, `^long`) in
|
||||
performance-critical paths to avoid reflection overhead.
|
||||
|
||||
## Code Conventions
|
||||
|
||||
### Namespace Overview
|
||||
|
||||
The source is located under `src` directory and this is a general overview of
|
||||
namespaces structure:
|
||||
|
||||
- `app.rpc.commands.*` – RPC command implementations (`auth`, `files`, `teams`, etc.)
|
||||
- `app.http.*` – HTTP routes and middleware
|
||||
- `app.db.*` – Database layer
|
||||
- `app.tasks.*` – Background job tasks
|
||||
- `app.main` – Integrant system setup and entrypoint
|
||||
- `app.loggers` – Internal loggers (auditlog, mattermost, etc.) (not to be confused with `app.common.logging`)
|
||||
|
||||
### RPC
|
||||
|
||||
The RPC methods are implemented using a multimethod-like structure via the
|
||||
`app.util.services` namespace. The main RPC methods are collected under
|
||||
`app.rpc.commands` namespace and exposed under `/api/rpc/command/<cmd-name>`.
|
||||
|
||||
The RPC method accepts POST and GET requests indistinctly and uses the `Accept`
|
||||
header to negotiate the response encoding (which can be Transit — the default —
|
||||
or plain JSON). It also accepts Transit (default) or JSON as input, which should
|
||||
be indicated using the `Content-Type` header.
|
||||
|
||||
The main convention is: use `get-` prefix on RPC name when we want READ
|
||||
operation.
|
||||
|
||||
Example of RPC method definition:
|
||||
|
||||
```clojure
|
||||
(sv/defmethod ::my-command
|
||||
{::rpc/auth true ;; requires auth
|
||||
::doc/added "1.18"
|
||||
::sm/params [:map ...] ;; malli input schema
|
||||
::sm/result [:map ...]} ;; malli output schema
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
;; return a plain map or throw
|
||||
{:id (uuid/next)})
|
||||
```
|
||||
|
||||
Look under `src/app/rpc/commands/*.clj` to see more examples.
|
||||
|
||||
### Tests
|
||||
|
||||
Test namespaces match `.*-test$` under `test/`. Config is in `tests.edn`.
|
||||
|
||||
|
||||
### Integrant System
|
||||
|
||||
The `src/app/main.clj` declares the system map. Each key is a component; values
|
||||
are config maps with `::ig/ref` for dependencies. Components implement
|
||||
`ig/init-key` / `ig/halt-key!`.
|
||||
|
||||
|
||||
### Connecting to the Database
|
||||
|
||||
Two PostgreSQL databases are used in this environment:
|
||||
|
||||
| Database | Purpose | Connection string |
|
||||
|---------------|--------------------|----------------------------------------------------|
|
||||
| `penpot` | Development / app | `postgresql://penpot:penpot@postgres/penpot` |
|
||||
| `penpot_test` | Test suite | `postgresql://penpot:penpot@postgres/penpot_test` |
|
||||
|
||||
**Interactive psql session:**
|
||||
|
||||
```bash
|
||||
# development DB
|
||||
psql "postgresql://penpot:penpot@postgres/penpot"
|
||||
|
||||
# test DB
|
||||
psql "postgresql://penpot:penpot@postgres/penpot_test"
|
||||
```
|
||||
|
||||
**One-shot query (non-interactive):**
|
||||
|
||||
```bash
|
||||
psql "postgresql://penpot:penpot@postgres/penpot" -c "SELECT id, name FROM team LIMIT 5;"
|
||||
```
|
||||
|
||||
**Useful psql meta-commands:**
|
||||
|
||||
```
|
||||
\dt -- list all tables
|
||||
\d <table> -- describe a table (columns, types, constraints)
|
||||
\di -- list indexes
|
||||
\q -- quit
|
||||
```
|
||||
|
||||
> **Migrations table:** Applied migrations are tracked in the `migrations` table
|
||||
> with columns `module`, `step`, and `created_at`. When renaming a migration
|
||||
> logical name, update this table in both databases to match the new name;
|
||||
> otherwise the runner will attempt to re-apply the migration on next startup.
|
||||
|
||||
```bash
|
||||
# Example: fix a renamed migration entry in the test DB
|
||||
psql "postgresql://penpot:penpot@postgres/penpot_test" \
|
||||
-c "UPDATE migrations SET step = 'new-name' WHERE step = 'old-name';"
|
||||
```
|
||||
|
||||
### Database Access (Clojure)
|
||||
|
||||
`app.db` wraps next.jdbc. Queries use a SQL builder that auto-converts kebab-case ↔ snake_case.
|
||||
|
||||
```clojure
|
||||
;; Query helpers
|
||||
(db/get cfg-or-pool :table {:id id}) ; fetch one row (throws if missing)
|
||||
(db/get* cfg-or-pool :table {:id id}) ; fetch one row (returns nil)
|
||||
(db/query cfg-or-pool :table {:team-id team-id}) ; fetch multiple rows
|
||||
(db/insert! cfg-or-pool :table {:name "x" :team-id id}) ; insert
|
||||
(db/update! cfg-or-pool :table {:name "y"} {:id id}) ; update
|
||||
(db/delete! cfg-or-pool :table {:id id}) ; delete
|
||||
|
||||
;; Run multiple statements/queries on single connection
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(db/insert! conn :table row1)
|
||||
(db/insert! conn :table row2))
|
||||
|
||||
|
||||
;; Transactions
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(db/insert! conn :table row)))
|
||||
```
|
||||
|
||||
Almost all methods in the `app.db` namespace accept `pool`, `conn`, or
|
||||
`cfg` as params.
|
||||
|
||||
Migrations live in `src/app/migrations/` as numbered SQL files. They run automatically on startup.
|
||||
|
||||
|
||||
### Error Handling
|
||||
|
||||
The exception helpers are defined on Common module, and are available under
|
||||
`app.common.exceptions` namespace.
|
||||
|
||||
Example of raising an exception:
|
||||
|
||||
```clojure
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "File does not exist"
|
||||
:file-id id)
|
||||
```
|
||||
|
||||
Common types: `:not-found`, `:validation`, `:authorization`, `:conflict`, `:internal`.
|
||||
|
||||
|
||||
### Performance Macros (`app.common.data.macros`)
|
||||
|
||||
Always prefer these macros over their `clojure.core` equivalents — they provide
|
||||
optimized implementations:
|
||||
|
||||
```clojure
|
||||
(dm/select-keys m [:a :b]) ;; faster than core/select-keys
|
||||
(dm/get-in obj [:a :b :c]) ;; faster than core/get-in
|
||||
(dm/str "a" "b" "c") ;; string concatenation
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
`src/app/config.clj` reads `PENPOT_*` environment variables, validated with
|
||||
Malli. Access anywhere via `(cf/get :smtp-host)`. Feature flags: `(cf/flags
|
||||
:enable-smtp)`.
|
||||
|
||||
|
||||
### Background Tasks
|
||||
|
||||
Background tasks live in `src/app/tasks/`. Each task is an Integrant component
|
||||
that exposes a `::handler` key and follows this three-method pattern:
|
||||
|
||||
```clojure
|
||||
(defmethod ig/assert-key ::handler ;; validate config at startup
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "expected a valid database pool"))
|
||||
|
||||
(defmethod ig/expand-key ::handler ;; inject defaults before init
|
||||
[k v]
|
||||
{k (assoc v ::my-option default-value)})
|
||||
|
||||
(defmethod ig/init-key ::handler ;; return the task fn
|
||||
[_ cfg]
|
||||
(fn [_task] ;; receives the task row from the worker
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
;; … do work …
|
||||
))))
|
||||
```
|
||||
|
||||
**Wiring a new task** requires two changes in `src/app/main.clj`:
|
||||
|
||||
1. **Handler config** – add an entry in `system-config` with the dependencies:
|
||||
|
||||
```clojure
|
||||
:app.tasks.my-task/handler
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
```
|
||||
|
||||
2. **Registry + cron** – register the handler name and schedule it:
|
||||
|
||||
```clojure
|
||||
;; in ::wrk/registry ::wrk/tasks map:
|
||||
:my-task (ig/ref :app.tasks.my-task/handler)
|
||||
|
||||
;; in worker-config ::wrk/cron ::wrk/entries vector:
|
||||
{:cron #penpot/cron "0 0 0 * * ?" ;; daily at midnight
|
||||
:task :my-task}
|
||||
```
|
||||
|
||||
**Useful cron patterns** (Quartz format — six fields: s m h dom mon dow):
|
||||
|
||||
| Expression | Meaning |
|
||||
|------------------------------|--------------------|
|
||||
| `"0 0 0 * * ?"` | Daily at midnight |
|
||||
| `"0 0 */6 * * ?"` | Every 6 hours |
|
||||
| `"0 */5 * * * ?"` | Every 5 minutes |
|
||||
|
||||
**Time helpers** (`app.common.time`):
|
||||
|
||||
```clojure
|
||||
(ct/now) ;; current instant
|
||||
(ct/duration {:hours 1}) ;; java.time.Duration
|
||||
(ct/minus (ct/now) some-duration) ;; subtract duration from instant
|
||||
```
|
||||
|
||||
`db/interval` converts a `Duration` (or millis / string) to a PostgreSQL
|
||||
interval object suitable for use in SQL queries:
|
||||
|
||||
```clojure
|
||||
(db/interval (ct/duration {:hours 1})) ;; → PGInterval "3600.0 seconds"
|
||||
```
|
||||
@ -1,70 +0,0 @@
|
||||
# Penpot Common – Agent Instructions
|
||||
|
||||
A shared module with code written in Clojure, ClojureScript, and
|
||||
JavaScript. Contains multiplatform code that can be used and executed
|
||||
from the frontend, backend, or exporter modules. It uses Clojure reader
|
||||
conditionals to specify platform-specific implementations.
|
||||
|
||||
## General Guidelines
|
||||
|
||||
To ensure consistency across the Penpot stack, all contributions must adhere to
|
||||
these criteria:
|
||||
|
||||
### 1. Testing & Validation
|
||||
|
||||
If code is added or modified in `src/`, corresponding tests in
|
||||
`test/common_tests/` must be added or updated.
|
||||
|
||||
* **Environment:** Tests should run in both JS (Node.js) and JVM environments.
|
||||
* **Location:** Place tests in the `test/common_tests/` directory, following the
|
||||
namespace structure of the source code (e.g., `app.common.colors` ->
|
||||
`common-tests.colors-test`).
|
||||
* **Execution:** Tests should be executed on both JS (Node.js) and JVM environments:
|
||||
* **Isolated:**
|
||||
* JS: To run a focused ClojureScript unit test: edit the
|
||||
`test/common_tests/runner.cljs` to narrow the test suite, then
|
||||
`pnpm run test:js`.
|
||||
* JVM: `pnpm run test:jvm --focus common-tests.my-ns-test`
|
||||
* **Regression:**
|
||||
* JS: Run `pnpm run test:js` without modifications on the runner (preferred)
|
||||
* JVM: Run `pnpm run test:jvm`
|
||||
|
||||
### 2. Code Quality & Formatting
|
||||
|
||||
* **Linting:** All code changes must pass linter checks:
|
||||
* Run `pnpm run lint:clj` for CLJ/CLJS/CLJC
|
||||
* **Formatting:** All code changes must pass the formatting check
|
||||
* Run `pnpm run check-fmt:clj` for CLJ/CLJS/CLJC
|
||||
* Run `pnpm run check-fmt:js` for JS
|
||||
* Use `pnpm run fmt` to fix all formatting issues (`pnpm run
|
||||
fmt:clj` or `pnpm run fmt:js` for isolated formatting fix).
|
||||
|
||||
## Code Conventions
|
||||
|
||||
### Namespace Overview
|
||||
|
||||
The source is located under `src` directory and this is a general overview of
|
||||
namespaces structure:
|
||||
|
||||
- `app.common.types.*` – Shared data types for shapes, files, pages using Malli schemas
|
||||
- `app.common.schema` – Malli abstraction layer, exposes the most used functions from malli
|
||||
- `app.common.geom.*` – Geometry and shape transformation helpers
|
||||
- `app.common.data` – Generic helpers used across the entire application
|
||||
- `app.common.math` – Generic math helpers used across the entire application
|
||||
- `app.common.json` – Generic JSON encoding/decoding helpers
|
||||
- `app.common.data.macros` – Performance macros used everywhere
|
||||
|
||||
|
||||
### Reader Conditionals
|
||||
|
||||
We use reader conditionals to differentiate implementations depending on the
|
||||
target platform where the code runs:
|
||||
|
||||
```clojure
|
||||
#?(:clj (import java.util.UUID)
|
||||
:cljs (:require [cljs.core :as core]))
|
||||
```
|
||||
|
||||
Both frontend and backend depend on `common` as a local library (`penpot/common
|
||||
{:local/root "../common"}`).
|
||||
|
||||
@ -1,371 +0,0 @@
|
||||
# Penpot Frontend – Agent Instructions
|
||||
|
||||
ClojureScript-based frontend application that uses React and RxJS as its main
|
||||
architectural pieces.
|
||||
|
||||
## General Guidelines
|
||||
|
||||
### 1. Testing & Validation
|
||||
|
||||
#### Unit Tests
|
||||
|
||||
If code is added or modified in `src/`, corresponding tests in
|
||||
`test/frontend_tests/` must be added or updated.
|
||||
|
||||
* **Environment:** Tests should run in a Node.js or browser-isolated
|
||||
environment without requiring the full application state or a
|
||||
running backend. Test are developed using cljs.test.
|
||||
* **Mocks & Stubs:** * Use proper mocks for any side-effecting
|
||||
functions (e.g., API calls, storage access).
|
||||
* Avoid testing through the UI (DOM); we have e2e tests for that.
|
||||
* Use `with-redefs` or similar ClojureScript mocking utilities to isolate the logic under test.
|
||||
* **No Flakiness:** Tests must be deterministic. Do not use `setTimeout` or real
|
||||
network calls. Use synchronous mocks for asynchronous workflows where
|
||||
possible.
|
||||
* **Location:** Place tests in the `test/frontend_tests/` directory, following the
|
||||
namespace structure of the source code (e.g., `app.utils.timers` ->
|
||||
`frontend-tests.util-timers-test`).
|
||||
* **Execution:**
|
||||
* **Isolated:** To run a focused ClojureScript unit test: edit the
|
||||
`test/frontend_tests/runner.cljs` to narrow the test suite, then `pnpm run
|
||||
test`.
|
||||
* **Regression:** To run `pnpm run test` without modifications on the runner (preferred)
|
||||
|
||||
|
||||
#### Integration Tests (Playwright)
|
||||
|
||||
Integration tests are developed under `frontend/playwright` directory, we use
|
||||
mocks for remote communication with the backend.
|
||||
|
||||
You should not add, modify or run the integration tests unless explicitly asked.
|
||||
|
||||
|
||||
```
|
||||
pnpm run test:e2e # Playwright e2e tests
|
||||
pnpm run test:e2e --grep "pattern" # Single e2e test by pattern
|
||||
```
|
||||
|
||||
Ensure everything is installed before executing tests with the `./scripts/setup` script.
|
||||
|
||||
|
||||
### 2. Code Quality & Formatting
|
||||
|
||||
* **Linting:** All code changes must pass linter checks:
|
||||
* Run `pnpm run lint:clj` for CLJ/CLJS/CLJC
|
||||
* Run `pnpm run lint:js` for JS
|
||||
* Run `pnpm run lint:scss` for SCSS
|
||||
* **Formatting:** All code changes must pass the formatting check
|
||||
* Run `pnpm run check-fmt:clj` for CLJ/CLJS/CLJC
|
||||
* Run `pnpm run check-fmt:js` for JS
|
||||
* Run `pnpm run check-fmt:scss` for SCSS
|
||||
* Use the `pnpm run fmt` fix all the formatting issues (`pnpm run fmt:clj`,
|
||||
`pnpm run fmt:js` or `pnpm run fmt:scss` for isolated formatting fix)
|
||||
|
||||
### 3. Implementation Rules
|
||||
|
||||
* **Logic vs. View:** If logic is embedded in a UI component, extract it into a
|
||||
function in the same namespace if it is only used locally, or look for a helper
|
||||
namespace to make it unit-testable.
|
||||
|
||||
### 4. Stack Trace Analysis
|
||||
|
||||
When analyzing production stack traces (minified code), you can generate a
|
||||
production bundle locally to map the minified code back to the source.
|
||||
|
||||
**To build the production bundle:**
|
||||
|
||||
Run: `pnpm run build:app`
|
||||
|
||||
The compiled files and their corresponding source maps will be generated in
|
||||
`resources/public/js`.
|
||||
|
||||
**Analysis Tips:**
|
||||
|
||||
- **Source Maps:** Use the `.map` files generated in `resources/public/js` with
|
||||
tools like `source-map-lookup` or browser dev tools to resolve minified
|
||||
locations.
|
||||
- **Bundle Inspection:** If the issue is related to bundle size or unexpected
|
||||
code inclusion, inspect the generated modules in `resources/public/js`.
|
||||
- **Shadow-CLJS Reports:** For more detailed analysis of what is included in the
|
||||
bundle, you can run shadow-cljs build reports (consult `shadow-cljs.edn` for
|
||||
build IDs like `main` or `worker`).
|
||||
|
||||
|
||||
## Code Conventions
|
||||
|
||||
### Namespace Overview
|
||||
|
||||
The source is located under `src` directory and this is a general overview of
|
||||
namespaces structure:
|
||||
|
||||
- `app.main.ui.*` – React UI components (`workspace`, `dashboard`, `viewer`)
|
||||
- `app.main.data.*` – Potok event handlers (state mutations + side effects)
|
||||
- `app.main.refs` – Reactive subscriptions (okulary lenses)
|
||||
- `app.main.store` – Potok event store
|
||||
- `app.util.*` – Utilities (DOM, HTTP, i18n, keyboard shortcuts)
|
||||
|
||||
|
||||
### State Management (Potok)
|
||||
|
||||
State is a single atom managed by a Potok store. Events implement protocols
|
||||
(funcool/potok library):
|
||||
|
||||
```clojure
|
||||
(defn my-event
|
||||
"doc string"
|
||||
[data]
|
||||
(ptk/reify ::my-event
|
||||
ptk/UpdateEvent
|
||||
(update [_ state] ;; synchronous state transition
|
||||
(assoc state :key data))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream] ;; async: returns an observable
|
||||
(->> (rp/cmd! :some-rpc-command params)
|
||||
(rx/map success-event)
|
||||
(rx/catch error-handler)))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _] ;; pure side effects (DOM, logging)
|
||||
(dom/focus (dom/get-element "id")))))
|
||||
```
|
||||
|
||||
The state is located under `app.main.store` namespace where we have
|
||||
the `emit!` function responsible for emitting events.
|
||||
|
||||
Example:
|
||||
|
||||
```cljs
|
||||
(ns some.ns
|
||||
(:require
|
||||
[app.main.data.my-events :refer [my-event]]
|
||||
[app.main.store :as st]))
|
||||
|
||||
(defn on-click
|
||||
[event]
|
||||
(st/emit! (my-event)))
|
||||
```
|
||||
|
||||
On `app.main.refs` we have reactive references which look up the main state
|
||||
for inner data or precalculated data. These references are very useful but
|
||||
should be used with care because, for example, if we have a complex operation,
|
||||
this operation will be executed on each state change. Sometimes it is better to
|
||||
have simple references and use React `use-memo` for more granular memoization.
|
||||
|
||||
Prefer helpers from `app.util.dom` instead of using direct DOM calls. If no
|
||||
helper is available, prefer adding a new helper and then using it.
|
||||
|
||||
### UI Components (React & Rumext: mf/defc)
|
||||
|
||||
The codebase contains various component patterns. When creating or refactoring
|
||||
components, follow the Modern Syntax rules outlined below.
|
||||
|
||||
#### 1. The * Suffix Convention
|
||||
|
||||
The most recent syntax uses a * suffix in the component name (e.g.,
|
||||
my-component*). This suffix signals the mf/defc macro to apply specific rules
|
||||
for props handling and destructuring and optimization.
|
||||
|
||||
#### 2. Component Definition
|
||||
|
||||
Modern components should use the following structure:
|
||||
|
||||
```clj
|
||||
(mf/defc my-component*
|
||||
{::mf/wrap [mf/memo]} ;; Equivalent to React.memo
|
||||
[{:keys [name on-click]}] ;; Destructured props
|
||||
[:div {:class (stl/css :root)
|
||||
:on-click on-click}
|
||||
name])
|
||||
```
|
||||
|
||||
#### 3. Hooks
|
||||
|
||||
Use the mf namespace for hooks to maintain consistency with the macro's
|
||||
lifecycle management. These are analogous to standard React hooks:
|
||||
|
||||
```clj
|
||||
(mf/use-state) ;; analogous to React.useState adapted to cljs semantics
|
||||
(mf/use-effect) ;; analogous to React.useEffect
|
||||
(mf/use-memo) ;; analogous to React.useMemo
|
||||
(mf/use-fn) ;; analogous to React.useCallback
|
||||
```
|
||||
|
||||
The `mf/use-state` in difference with React.useState, returns an atom-like
|
||||
object, where you can use `swap!` or `reset!` to perform an update and
|
||||
`deref` to get the current value.
|
||||
|
||||
You also have the `mf/deref` hook (which does not follow the `use-` naming
|
||||
pattern) and its purpose is to watch (subscribe to changes on) an atom or
|
||||
derived atom (from okulary) and get the current value. It is mainly used to
|
||||
subscribe to lenses defined in `app.main.refs` or private lenses defined in
|
||||
namespaces.
|
||||
|
||||
Rumext also comes with improved syntax macros as alternative to `mf/use-effect`
|
||||
and `mf/use-memo` functions. Examples:
|
||||
|
||||
|
||||
Example for `mf/with-effect` macro:
|
||||
|
||||
```clj
|
||||
;; Using functions
|
||||
(mf/use-effect
|
||||
(mf/deps team-id)
|
||||
(fn []
|
||||
(st/emit! (dd/initialize team-id))
|
||||
(fn []
|
||||
(st/emit! (dd/finalize team-id)))))
|
||||
|
||||
;; The same effect but using mf/with-effect
|
||||
(mf/with-effect [team-id]
|
||||
(st/emit! (dd/initialize team-id))
|
||||
(fn []
|
||||
(st/emit! (dd/finalize team-id))))
|
||||
```
|
||||
|
||||
Example for `mf/with-memo` macro:
|
||||
|
||||
```
|
||||
;; Using functions
|
||||
(mf/use-memo
|
||||
(mf/deps projects team-id)
|
||||
(fn []
|
||||
(->> (vals projects)
|
||||
(filterv #(= team-id (:team-id %))))))
|
||||
|
||||
;; Using the macro
|
||||
(mf/with-memo [projects team-id]
|
||||
(->> (vals projects)
|
||||
(filterv #(= team-id (:team-id %)))))
|
||||
```
|
||||
|
||||
Prefer using the macros for their syntax simplicity.
|
||||
|
||||
|
||||
#### 4. Component Usage (Hiccup Syntax)
|
||||
|
||||
When invoking a component within Hiccup, always use the [:> component* props]
|
||||
pattern.
|
||||
|
||||
Requirements for props:
|
||||
|
||||
- Must be a map literal or a symbol pointing to a JavaScript props object.
|
||||
- To create a JS props object, use the `#js` literal or the `mf/spread-object` helper macro.
|
||||
|
||||
Examples:
|
||||
|
||||
```clj
|
||||
;; Using object literal (no need of #js because macro already interprets it)
|
||||
[:> my-component* {:data-foo "bar"}]
|
||||
|
||||
;; Using object literal (no need of #js because macro already interprets it)
|
||||
(let [props #js {:data-foo "bar"
|
||||
:className "myclass"}]
|
||||
[:> my-component* props])
|
||||
|
||||
;; Using the spread helper
|
||||
(let [props (mf/spread-object base-props {:extra "data"})]
|
||||
[:> my-component* props])
|
||||
```
|
||||
|
||||
#### 5. Styles
|
||||
|
||||
##### Styles on component code
|
||||
Styles are co-located with components. Each `.cljs` file has a corresponding
|
||||
`.scss` file.
|
||||
|
||||
Example of clojurescript code for reference classes defined on styles (we use
|
||||
CSS modules pattern):
|
||||
|
||||
```clojure
|
||||
;; In the component namespace:
|
||||
(require '[app.main.style :as stl])
|
||||
|
||||
;; In the render function:
|
||||
[:div {:class (stl/css :container :active)}]
|
||||
|
||||
;; Conditional:
|
||||
[:div {:class (stl/css-case :some-class true :selected (= drawtool :rect))}]
|
||||
|
||||
;; When you need concat an existing class:
|
||||
[:div {:class [existing-class (stl/css-case :some-class true :selected (= drawtool :rect))]}]
|
||||
```
|
||||
|
||||
##### General rules for styling
|
||||
|
||||
- Prefer CSS custom properties ( `margin: var(--sp-xs);`) instead of scss
|
||||
variables and get the already defined properties from `_sizes.scss`. The SCSS
|
||||
variables are allowed and still used, just prefer properties if they are
|
||||
already defined.
|
||||
- If a value isn't in the DS, use the `px2rem(n)` mixin: `@use "ds/_utils.scss"
|
||||
as *; padding: px2rem(23);`.
|
||||
- Do **not** create new SCSS variables for one-off values.
|
||||
- Use physical directions with logical ones to support RTL/LTR naturally:
|
||||
- Avoid: `margin-left`, `padding-right`, `left`, `right`.
|
||||
- Prefer: `margin-inline-start`, `padding-inline-end`, `inset-inline-start`.
|
||||
- Always use the `use-typography` mixin from `ds/typography.scss`:
|
||||
- Example: `@include t.use-typography("title-small");`
|
||||
- Use `$br-*` for radius and `$b-*` for thickness from `ds/_borders.scss`.
|
||||
- Use only tokens from `ds/colors.scss`. Do **NOT** use `design-tokens.scss` or
|
||||
legacy color variables.
|
||||
- Use mixins only from `ds/mixins.scss`. Avoid legacy mixins like
|
||||
`@include flexCenter;`. Write standard CSS (flex/grid) instead.
|
||||
- Use the `@use` instead of `@import`. If you go to refactor existing SCSS file,
|
||||
try to replace all `@import` with `@use`. Example: `@use "ds/_sizes.scss" as
|
||||
*;` (Use `as *` to expose variables directly).
|
||||
- Avoid deep selector nesting or high-specificity (IDs). Flatten selectors:
|
||||
- Avoid: `.card { .title { ... } }`
|
||||
- Prefer: `.card-title { ... }`
|
||||
- Leverage component-level CSS variables for state changes (hover/focus) instead
|
||||
of rewriting properties.
|
||||
|
||||
##### Checklist
|
||||
|
||||
- [ ] No references to `common/refactor/`
|
||||
- [ ] All `@import` converted to `@use` (only if refactoring)
|
||||
- [ ] Physical properties (left/right) using logical properties (inline-start/end).
|
||||
- [ ] Typography implemented via `use-typography()` mixin.
|
||||
- [ ] Hardcoded pixel values wrapped in `px2rem()`.
|
||||
- [ ] Selectors are flat (no deep nesting).
|
||||
|
||||
|
||||
### Translations (`tr`) and Memoization
|
||||
|
||||
`(tr "some.key")` resolves the translation string from the **currently active
|
||||
locale at call time**. This has two consequences:
|
||||
|
||||
- **Never call `(tr ...)` at namespace level** (inside a `def` or `defonce`).
|
||||
Doing so would freeze the label to the locale active at module load time and
|
||||
break runtime language switching.
|
||||
- **Always call `(tr ...)` at render time** — either directly in the component
|
||||
body or inside a `mf/with-memo` / `mf/use-memo` block.
|
||||
|
||||
When a component renders a **static list of options** whose labels come from
|
||||
`(tr ...)` (e.g. radio button options, select options), wrap the vector in
|
||||
`mf/with-memo []` with no dependencies. This ensures the vector and its
|
||||
`(tr ...)` calls are evaluated once per component mount instead of on every
|
||||
render, while still respecting the render-time requirement:
|
||||
|
||||
```clojure
|
||||
(let [options (mf/with-memo []
|
||||
[{:value "top" :label (tr "some.key.top")}
|
||||
{:value "center" :label (tr "some.key.center")}
|
||||
{:value "bottom" :label (tr "some.key.bottom")}])]
|
||||
...)
|
||||
```
|
||||
|
||||
### Performance Macros (`app.common.data.macros`)
|
||||
|
||||
Always prefer these macros over their `clojure.core` equivalents — they compile to faster JavaScript:
|
||||
|
||||
```clojure
|
||||
(dm/select-keys m [:a :b]) ;; ~6x faster than core/select-keys
|
||||
(dm/get-in obj [:a :b :c]) ;; faster than core/get-in
|
||||
(dm/str "a" "b" "c") ;; string concatenation
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
`src/app/config.clj` reads globally defined variables and exposes precomputed
|
||||
configuration values ready to be used from other parts of the application.
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
# Code Style and Conventions
|
||||
|
||||
## General Principles
|
||||
- **Object-Oriented Design**: VERY IMPORTANT: Use idiomatic, object-oriented style with explicit abstractions
|
||||
- **Strategy Pattern**: Prefer explicitly typed interfaces over bare functions for non-trivial functionality
|
||||
- **Clean Architecture**: Tools implement a common interface for consistent registration and execution
|
||||
|
||||
## TypeScript Configuration
|
||||
- **Strict Mode**: All strict TypeScript options enabled
|
||||
- **Target**: ES2022
|
||||
- **Module System**: CommonJS
|
||||
- **Declaration Files**: Generated with source maps
|
||||
|
||||
## Naming Conventions
|
||||
- **Classes**: PascalCase (e.g., `ExeceuteCodeTool`, `PenpotMcpServer`)
|
||||
- **Interfaces**: PascalCase (e.g., `Tool`)
|
||||
- **Methods**: camelCase (e.g., `execute`, `registerTools`)
|
||||
- **Constants**: camelCase for readonly properties (e.g., `definition`)
|
||||
- **Files**: PascalCase for classes (e.g., `ExecuteCodeTool.ts`)
|
||||
|
||||
## Documentation Style
|
||||
- **JSDoc**: Use comprehensive JSDoc comments for classes, methods, and interfaces
|
||||
- **Description Format**: Initial elliptical phrase that defines *what* it is, followed by details
|
||||
- **Comment Style**: VERY IMPORTANT: Start with lowercase for comments of code blocks (unless lengthy explanation with multiple sentences)
|
||||
|
||||
@ -1,53 +1,69 @@
|
||||
# Penpot MCP Project Overview - Updated
|
||||
# Penpot MCP
|
||||
|
||||
## Purpose
|
||||
This project is a Model Context Protocol (MCP) server for Penpot integration.
|
||||
This subproject provides an MCP server for Penpot integration.
|
||||
The MCP server communicates with a Penpot plugin via WebSockets, allowing
|
||||
the MCP server to send tasks to the plugin and receive results,
|
||||
the MCP server to send tasks to the plugin and receive results,
|
||||
enabling advanced AI-driven features in Penpot.
|
||||
|
||||
## Tech Stack
|
||||
- **Language**: TypeScript
|
||||
- **Runtime**: Node.js
|
||||
- **Framework**: MCP SDK (@modelcontextprotocol/sdk)
|
||||
- **Build Tool**: TypeScript Compiler (tsc) + esbuild
|
||||
- **Package Manager**: pnpm
|
||||
- **WebSocket**: ws library for real-time communication
|
||||
|
||||
## Project Structure
|
||||
- Language: TypeScript
|
||||
- Runtime: Node.js
|
||||
- Framework: MCP SDK (@modelcontextprotocol/sdk)
|
||||
- Build Tool: TypeScript Compiler (tsc) + esbuild
|
||||
- Package Manager: pnpm
|
||||
|
||||
## General Principles
|
||||
|
||||
IMPORTANT: Use an idiomatic, object-oriented style.
|
||||
In particular, this implies that, for any non-trivial interfaces, you use interfaces that expect explicitly typed abstractions
|
||||
rather than mere functions (i.e. use the strategy pattern, for example).
|
||||
|
||||
Comments:
|
||||
When describing parameters, methods/functions and classes, you use a precise style, where the initial (elliptical) phrase
|
||||
clearly defines *what* it is. Any details then follow in subsequent sentences.
|
||||
|
||||
When describing what blocks of code do, you also use an elliptical style and start with a lower-case letter unless
|
||||
the comment is a lengthy explanation with at least two sentences (in which case you start with a capital letter, as is
|
||||
required for sentences).
|
||||
|
||||
## Project Structure (Excerpt)
|
||||
|
||||
```
|
||||
/ (project root)
|
||||
├── packages/common/ # Shared type definitions
|
||||
│ ├── src/
|
||||
│ │ ├── index.ts # Exports for shared types
|
||||
│ │ ├── index.ts # exports for shared types
|
||||
│ │ └── types.ts # PluginTaskResult, request/response interfaces
|
||||
│ └── package.json # @penpot-mcp/common package
|
||||
├── packages/server/ # Main MCP server implementation
|
||||
├── packages/server/ # MCP server subproject
|
||||
│ ├── src/
|
||||
│ │ ├── index.ts # Main server entry point
|
||||
│ │ ├── PenpotMcpServer.ts # Enhanced with request/response correlation
|
||||
│ │ ├── PluginTask.ts # Now supports result promises
|
||||
│ │ ├── index.ts # entry point
|
||||
│ │ ├── PenpotMcpServer.ts # MCP server implementation (connection handling, tool registration, etc.)
|
||||
│ │ ├── Tool.ts # base class for tools
|
||||
│ │ ├── PluginTask.ts # base class for plugin tasks
|
||||
│ │ ├── tasks/ # PluginTask implementations
|
||||
│ │ └── tools/ # Tool implementations
|
||||
| ├── data/ # Contains resources, such as API info and prompts
|
||||
│ └── package.json # Includes @penpot-mcp/common dependency
|
||||
├── packages/plugin/ # Penpot plugin with response capability
|
||||
| ├── data/ # contains resources, such as API info and prompts
|
||||
│ └── package.json
|
||||
├── packages/plugin/ # Penpot plugin subproject
|
||||
│ ├── src/
|
||||
│ │ ├── main.ts # Enhanced WebSocket handling with response forwarding
|
||||
│ │ └── plugin.ts # Now sends task responses back to server
|
||||
│ │ ├── main.ts # handles communication
|
||||
│ │ └── plugin.ts # plugin implementation
|
||||
│ └── package.json # Includes @penpot-mcp/common dependency
|
||||
└── prepare-api-docs # Python project for the generation of API docs
|
||||
```
|
||||
|
||||
## Key Tasks
|
||||
## Key Development Tasks
|
||||
|
||||
### Adjusting the System Prompt
|
||||
### Adjusting the Prompts
|
||||
|
||||
The system prompt file is located in `packages/server/data/initial_instructions.md`.
|
||||
The system prompt file (aka Penpot High-Level Overview) is located in
|
||||
`packages/server/data/initial_instructions.md`.
|
||||
|
||||
### Adding a new Tool
|
||||
|
||||
1. Implement the tool class in `packages/server/src/tools/` following the `Tool` interface.
|
||||
1. Implement the tool class in `packages/server/src/tools/` following the `Tool` interface.
|
||||
IMPORTANT: Do not catch any exceptions in the `executeCore` method. Let them propagate to be handled centrally.
|
||||
2. Register the tool in `PenpotMcpServer`.
|
||||
|
||||
@ -62,3 +78,10 @@ Many tools build on `ExecuteCodePluginTask`, as many operations can be reduced t
|
||||
* In the success case, call `task.sendSuccess`.
|
||||
* In the failure case, just throw an exception, which will be handled centrally!
|
||||
4. Register the task handler in `packages/plugin/src/plugin.ts` in the `taskHandlers` list.
|
||||
|
||||
## Dev Tooling
|
||||
|
||||
From the project root directory, run
|
||||
|
||||
* `pnpm run build` to test the build of all package
|
||||
* `pnpm run fmt` to apply the auto-formatter
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
# Suggested Commands
|
||||
|
||||
## Development Commands
|
||||
```bash
|
||||
# Navigate to MCP server directory
|
||||
cd penpot/mcp/server
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Build the TypeScript project
|
||||
pnpm run build
|
||||
|
||||
# Start the server (production)
|
||||
pnpm run start
|
||||
|
||||
# Start the server in development mode
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
## Testing and Development
|
||||
```bash
|
||||
# Run TypeScript compiler in watch mode
|
||||
pnpx tsc --watch
|
||||
|
||||
# Check TypeScript compilation without emitting files
|
||||
pnpx tsc --noEmit
|
||||
```
|
||||
|
||||
## Windows-Specific Commands
|
||||
```cmd
|
||||
# Directory navigation
|
||||
cd penpot/mcp/server
|
||||
dir # List directory contents
|
||||
type package.json # Display file contents
|
||||
|
||||
# Git operations
|
||||
git status
|
||||
git add .
|
||||
git commit -m "message"
|
||||
git push
|
||||
|
||||
# File operations
|
||||
copy src\file.ts backup\file.ts # Copy files
|
||||
del dist\* # Delete files
|
||||
mkdir new-directory # Create directory
|
||||
rmdir /s directory # Remove directory recursively
|
||||
```
|
||||
|
||||
## Project Structure Navigation
|
||||
```bash
|
||||
# Key directories
|
||||
cd penpot/mcp/server/src # Source code
|
||||
cd penpot/mcp/server/src/tools # Tool implementations
|
||||
cd penpot/mcp/server/src/interfaces # Type definitions
|
||||
cd penpot/mcp/server/dist # Compiled output
|
||||
```
|
||||
|
||||
## Common Utilities
|
||||
```cmd
|
||||
# Search for text in files
|
||||
findstr /s /i "HelloWorld" *.ts
|
||||
|
||||
# Find files by name
|
||||
dir /s /b *Tool.ts
|
||||
|
||||
# Process management
|
||||
tasklist | findstr node # Find Node.js processes
|
||||
taskkill /f /im node.exe # Kill Node.js processes
|
||||
```
|
||||
@ -1,56 +0,0 @@
|
||||
# Task Completion Guidelines
|
||||
|
||||
## After Making Code Changes
|
||||
|
||||
### 1. Build and Test
|
||||
```bash
|
||||
cd mcp-server
|
||||
npm run build:full # or npm run build for faster bundling only
|
||||
```
|
||||
|
||||
### 2. Verify TypeScript Compilation
|
||||
```bash
|
||||
npx tsc --noEmit
|
||||
```
|
||||
|
||||
### 3. Test the Server
|
||||
```bash
|
||||
# Start in development mode to test changes
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 4. Code Quality Checks
|
||||
- Ensure all code follows the established conventions
|
||||
- Verify JSDoc comments are complete and accurate
|
||||
- Check that error handling is appropriate
|
||||
- Use clean imports WITHOUT file extensions (esbuild handles resolution)
|
||||
- Validate that tool interfaces are properly implemented
|
||||
|
||||
### 5. Integration Testing
|
||||
- Test tool registration in the main server
|
||||
- Verify MCP protocol compliance
|
||||
- Ensure tool definitions match implementation
|
||||
|
||||
## Before Committing Changes
|
||||
1. **Build Successfully**: `npm run build:full` completes without errors
|
||||
2. **No TypeScript Errors**: `npx tsc --noEmit` passes
|
||||
3. **Documentation Updated**: JSDoc comments reflect changes
|
||||
4. **Tool Registry Updated**: New tools added to `registerTools()` method
|
||||
5. **Interface Compliance**: All tools implement the `Tool` interface correctly
|
||||
|
||||
## File Organization
|
||||
- Place new tools in `src/tools/` directory
|
||||
- Update main server registration in `src/index.ts`
|
||||
- Follow existing naming conventions
|
||||
|
||||
## Common Patterns
|
||||
- All tools must implement the `Tool` interface
|
||||
- Use readonly properties for tool definitions
|
||||
- Include comprehensive error handling
|
||||
- Follow the established documentation style
|
||||
- Import WITHOUT file extensions (esbuild resolves them automatically)
|
||||
|
||||
## Build System
|
||||
- Uses esbuild for fast bundling and TypeScript for declarations
|
||||
- Import statements should omit file extensions entirely
|
||||
- IDE refactoring is safe - no extension-related build failures
|
||||
@ -1,11 +1,10 @@
|
||||
# whether to use the project's gitignore file to ignore files
|
||||
# Added on 2025-04-07
|
||||
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
# list of additional paths to ignore
|
||||
# same syntax as gitignore, so you can use * and **
|
||||
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||
# Added (renamed) on 2025-04-07
|
||||
# list of additional paths to ignore in this project.
|
||||
# Same syntax as gitignore, so you can use * and **.
|
||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
@ -13,65 +12,15 @@ ignored_paths: []
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
# list of tool names to exclude.
|
||||
# This extends the existing exclusions (e.g. from the global configuration)
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
excluded_tools: []
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: |
|
||||
IMPORTANT: You use an idiomatic, object-oriented style.
|
||||
In particular, this implies that, for any non-trivial interfaces, you use interfaces that expect explicitly typed abstractions
|
||||
rather than mere functions (i.e. use the strategy pattern, for example).
|
||||
|
||||
Always read the "project_overview" memory.
|
||||
|
||||
Comments:
|
||||
When describing parameters, methods/functions and classes, you use a precise style, where the initial (elliptical) phrase
|
||||
clearly defines *what* it is. Any details then follow in subsequent sentences.
|
||||
|
||||
When describing what blocks of code do, you also use an elliptical style and start with a lower-case letter unless
|
||||
the comment is a lengthy explanation with at least two sentences (in which case you start with a capital letter, as is
|
||||
required for sentences).
|
||||
CRITICAL: Always read the "project_overview" memory.
|
||||
# the name by which the project can be referenced within Serena
|
||||
project_name: "penpot-mcp"
|
||||
|
||||
@ -83,18 +32,24 @@ project_name: "penpot-mcp"
|
||||
# Set this to a list of mode names to always include the respective modes for this project.
|
||||
base_modes:
|
||||
|
||||
# list of mode names that are to be activated by default.
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# list of mode names that are to be activated by default, overriding the setting in the global configuration.
|
||||
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
||||
# If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||
# Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply
|
||||
# for this project.
|
||||
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
default_modes:
|
||||
|
||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
|
||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
|
||||
# This extends the existing inclusions (e.g. from the global configuration).
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
included_optional_tools: []
|
||||
|
||||
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
||||
fixed_tools: []
|
||||
|
||||
# the encoding used by text files in the project
|
||||
@ -102,23 +57,28 @@ fixed_tools: []
|
||||
encoding: utf-8
|
||||
|
||||
|
||||
|
||||
# list of languages for which language servers are started; choose from:
|
||||
# al bash clojure cpp csharp
|
||||
# csharp_omnisharp dart elixir elm erlang
|
||||
# fortran fsharp go groovy haskell
|
||||
# java julia kotlin lua markdown
|
||||
# matlab nix pascal perl php
|
||||
# powershell python python_jedi r rego
|
||||
# ruby ruby_solargraph rust scala swift
|
||||
# terraform toml typescript typescript_vts vue
|
||||
# yaml zig
|
||||
# al angular ansible bash clojure
|
||||
# cpp cpp_ccls crystal csharp csharp_omnisharp
|
||||
# dart elixir elm erlang fortran
|
||||
# fsharp go groovy haskell haxe
|
||||
# hlsl html java json julia
|
||||
# kotlin lean4 lua luau markdown
|
||||
# matlab msl nix ocaml pascal
|
||||
# perl php php_phpactor powershell python
|
||||
# python_jedi python_ty r rego ruby
|
||||
# ruby_solargraph rust scala scss solidity
|
||||
# svelte swift systemverilog terraform toml
|
||||
# typescript typescript_vts vue yaml zig
|
||||
# (This list may be outdated. For the current list, see values of Language enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
||||
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
|
||||
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
|
||||
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
|
||||
# - For Free Pascal/Lazarus, use pascal
|
||||
# Special requirements:
|
||||
# Some languages require additional setup/installations.
|
||||
@ -164,3 +124,19 @@ ignored_memory_patterns: []
|
||||
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
|
||||
# No documentation on options means no options are available.
|
||||
ls_specific_settings: {}
|
||||
|
||||
# list of mode names to be activated additionally for this project, e.g. ["query-projects"]
|
||||
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
added_modes:
|
||||
|
||||
# list of additional workspace folder paths for cross-package reference support (e.g. in monorepos).
|
||||
# Paths can be absolute or relative to the project root.
|
||||
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
|
||||
# symbols and references across package boundaries.
|
||||
# Currently supported for: TypeScript.
|
||||
# Example:
|
||||
# additional_workspace_folders:
|
||||
# - ../sibling-package
|
||||
# - ../shared-lib
|
||||
additional_workspace_folders: []
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
# render-wasm – Agent Instructions
|
||||
|
||||
This component compiles Rust to WebAssembly using Emscripten +
|
||||
Skia. It is consumed by the frontend as a canvas renderer.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
./build # Compile Rust → WASM (requires Emscripten environment)
|
||||
./watch # Incremental rebuild on file change
|
||||
./test # Run Rust unit tests (cargo test)
|
||||
./lint # clippy -D warnings
|
||||
cargo fmt --check
|
||||
```
|
||||
|
||||
Run a single test:
|
||||
```bash
|
||||
cargo test my_test_name # by test function name
|
||||
cargo test shapes:: # by module prefix
|
||||
```
|
||||
|
||||
Build output lands in `../frontend/resources/public/js/` (consumed directly by the frontend dev server).
|
||||
|
||||
## Build Environment
|
||||
|
||||
The `_build_env` script sets required env vars (Emscripten paths,
|
||||
`EMCC_CFLAGS`). `./build` sources it automatically. The WASM heap is
|
||||
configured to 256 MB initial with geometric growth.
|
||||
|
||||
## Architecture
|
||||
|
||||
**Global state** — a single `unsafe static mut State` accessed
|
||||
exclusively through `with_state!` / `with_state_mut!` macros. Never
|
||||
access it directly.
|
||||
|
||||
**Tile-based rendering** — only 512×512 tiles within the viewport
|
||||
(plus a pre-render buffer) are drawn each frame. Tiles outside the
|
||||
range are skipped.
|
||||
|
||||
**Two-phase updates** — shape data is written via exported setter
|
||||
functions (called from ClojureScript), then a single `render_frame()`
|
||||
triggers the actual Skia draw calls.
|
||||
|
||||
**Shape hierarchy** — shapes live in a flat pool indexed by UUID;
|
||||
parent/child relationships are tracked separately.
|
||||
|
||||
## Key Source Modules
|
||||
|
||||
| Path | Role |
|
||||
|------|------|
|
||||
| `src/lib.rs` | WASM exports — all functions callable from JS |
|
||||
| `src/state.rs` | Global `State` struct definition |
|
||||
| `src/render/` | Tile rendering pipeline, Skia surface management |
|
||||
| `src/shapes/` | Shape types and Skia draw logic per shape |
|
||||
| `src/wasm/` | JS interop helpers (memory, string encoding) |
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
The WASM module is loaded by `app.render-wasm.*` namespaces in the
|
||||
frontend. ClojureScript calls exported Rust functions to push shape
|
||||
data, then calls `render_frame`. Do not change export function
|
||||
signatures without updating the corresponding ClojureScript bridge.
|
||||
Loading…
x
Reference in New Issue
Block a user