504 Commits

Author SHA1 Message Date
Miguel de Benito Delgado
036236400d 🚧 WIP 25 complete runner, drop pilot
AI-assisted-by: deepseek-flash, gpt-6.1-sol
2026-10-28 19:17:39 +01:00
Miguel de Benito Delgado
36d696c345 🚧 WIP 24 Relocate all code, delete old benchmarks 2026-10-28 19:17:39 +01:00
Miguel de Benito Delgado
35cd79cb03 🚧 WIP 23 Results and reporting. Cleanup 2026-10-28 19:17:39 +01:00
Miguel de Benito Delgado
d5272e3179 🚧 WIP Step 22 nicer macros 2026-10-28 19:17:39 +01:00
Miguel de Benito Delgado
693b0f5706 🚧 WIP Step 21.2 Cosmetic simplifications 2026-10-28 19:17:39 +01:00
Miguel de Benito Delgado
dc22e2d77f 🚧 WIP Step 21 Refactor namespaces 2026-10-28 19:17:39 +01:00
Miguel de Benito Delgado
23746893e6 🚧 WIP Step 20 test last round of features 2026-10-28 19:17:39 +01:00
Miguel de Benito Delgado
5ca9e06fa7 🚧 WIP Step 16.4 Refactor failures, remove legacy validation, cleanup 2026-10-28 19:17:38 +01:00
Miguel de Benito Delgado
6cec9d7be4 🚧 WIP Step 16 Introduce hooks for the browser and load-scene 2026-10-28 19:17:38 +01:00
Miguel de Benito Delgado
b982e864bd 🚧 WIP Step 15, Use transit to simplify validation 2026-10-28 19:17:38 +01:00
Miguel de Benito Delgado
634b74d0c6 🚧 WIP Step 14, Grab correct w/h attrs from context 2026-10-28 19:17:38 +01:00
Miguel de Benito Delgado
680408796a 🚧 WIP Step 13, Track failure causes
AI-assisted-by: muse-spark, GLM-5.3
2026-10-28 19:17:38 +01:00
Miguel de Benito Delgado
5224a54cb7 🚧 WIP Step 12.3, Test cleanup + missing coverage
AI-assisted-by: muse-spark, deepseek-flash
2026-10-28 19:17:38 +01:00
Miguel de Benito Delgado
9f945de1a6 🚧 WIP Step 12.2, Fix browser bridge, lifecycle
AI-assisted-by: muse-spark, glm-5.3
2026-10-28 19:17:38 +01:00
Miguel de Benito Delgado
5afac4a330 🚧 WIP Step 11, browser bridge
- New schema:viewport
- Browser bridge (with some temp code)
- Move prepare-scene out measured regions
-
AI-assisted-by: muse-spark-1.3, deepseek-flash, glm-5.3
2026-10-28 19:17:38 +01:00
Miguel de Benito Delgado
4e9d83b4ad 🚧 WIP Step 10, scene upload 2026-10-28 19:17:38 +01:00
Miguel de Benito Delgado
d46f5e59c6 🚧 WIP Step 10, defcase body validation, docs 2026-10-28 19:17:38 +01:00
Miguel de Benito Delgado
8d04530e07 🚧 WIP Step 9, fix naming conventions 2026-10-28 19:17:38 +01:00
Miguel de Benito Delgado
c16233f841 🚧 WIP Step 8, simplify and clean up tests 2026-10-28 19:17:38 +01:00
Miguel de Benito Delgado
44dfb47c98 🚧 WIP Step 7, scene and case declarations 2026-10-28 19:17:38 +01:00
Miguel de Benito Delgado
551c3bffb7 🚧 WIP Step 6, ellipses 2026-10-28 19:17:38 +01:00
Miguel de Benito Delgado
bfde40d1ab 🚧 WIP Step 5, bools 2026-10-28 19:17:38 +01:00
Miguel de Benito Delgado
bd17e6d431 🚧 WIP Step 4, frames and groups 2026-10-28 19:17:38 +01:00
Miguel de Benito Delgado
9d281b225e 🚧 WIP Step 3, fixture macros, prng, defaults, placeholder scene 2026-10-28 19:17:38 +01:00
Miguel de Benito Delgado
01f6eb2a7a 🚧 WIP Step 2, common scene code, validation 2026-10-28 19:17:38 +01:00
Dr. Dominik Jain
e20a0d748e
✨ Add OAuth support for MCP server (#12480)
* 🎉 Add OAuth support for remote MCP connections

Add MCP-specific discovery, client registration, and approval endpoints.
Exchange single-use authorization codes with PKCE for the fixed MCP user
token, enabling MCP and creating the token on approval when needed.

Use self-contained client IDs without new database tables or stored client
records. Keep temporary authorization state in Redis/Valkey.

Accept bearer credentials while preserving query-parameter connections.
Enable OAuth through PENPOT_MCP_AUTH_URI in multi-user mode. Document the
environment variable in the README and implementation details in Serena.

Explain protocol and concurrency decisions in the Clojure code. Cover token
provisioning, OAuth state, client registration, HTTP flows, and the approval
page with tests.

AI-assisted-by: gpt-6

* 🐛 Wait for devenv initialization before starting builds

Docker reports a container as Running as soon as its main process starts,
while the entrypoint may still be preparing the development environment.
manage.sh used that status to launch tmux jobs, allowing frontend builds
to race with ownership changes to /opt/rustup and Cargo setup.

On a fresh container, rustup could fail with Permission denied before the
frontend watcher started. Nginx would later serve the existing JavaScript,
leaving source changes absent from the running app despite restarting
devenv.

Wait for nginx's local health endpoint before launching the jobs. Nginx
starts after entrypoint setup, so its response confirms that setup has
finished. Allow up to 120 seconds and report a timeout without starting
the jobs if initialization does not complete.

Validation: shell syntax and whitespace checks pass.

AI-assisted-by: gpt-6

* ♻️ Share the login modal with MCP authorization

Extract the viewer login modal and its styles into a shared auth
component, retaining the viewer wrapper and its close behavior.

Use the modal on the MCP approval page and preserve the pending request
through login. Closing it clears the saved return URL and shows a
cancellation message without further actions.

Cover return URL handling and cancellation with frontend tests.

AI-assisted-by: gpt-6

* ✨ Show client icons in MCP authorization

Preserve HTTPS logo URLs from client registration and published metadata
and show the icon beside the application name on the approval page.
Ignore unusable URLs and hide failed images without blocking approval.

Update the access description to refer to MCP connections established
from Penpot. Cover logo metadata and approval responses in backend tests.

AI-assisted-by: gpt-6

* 🐛 Follow the system theme when no preference is configured

Default to the system theme in the shared resolver used by the app and
plugins. Preserve explicit light and dark choices, including the legacy
default value that means dark.

Reuse the reactive theme hook in progress widgets and show the system
preference in settings for accounts without a configured theme.

Verify preference resolution with regression tests and browser checks.
All 1,474 frontend tests pass.

AI-assisted-by: gpt-6

* 🐛 Replace expired MCP tokens during OAuth approval

Settings can give an MCP token an expiry date. OAuth approval reused
the newest MCP token without checking it, so an expired token reached
the client and never matched a plugin connection.

Approval now treats an expired token as missing and creates a new one.
Token exchange rejects a code whose token expired after approval with
invalid_grant.

AI-assisted-by: claude-opus-5-5

* 🐛 Log unexpected errors on MCP OAuth endpoints

The OAuth routes sit outside the shared HTTP middleware, so the shared
error handler never saw them, and their own handler turned unexpected
failures into a 503 without logging anything. Failures in the session
middleware of the approval endpoints had no handler at all.

Unexpected failures now also go through the shared error handler for
its logging, while clients still get an OAuth error body. The OAuth
error handler also wraps the session middleware of the approval
endpoints.

AI-assisted-by: claude-opus-5-5

* 🔧 Add devenv script to run the MCP server with OAuth

start-mcp-devenv-oauth starts the MCP server in multi-user mode with
PENPOT_MCP_AUTH_URI derived from PENPOT_PUBLIC_URI, to test OAuth in
the devenv. Agentic tooling still uses the single-user
start-mcp-devenv.

AI-assisted-by: claude-opus-5-5

* 🐛 Fix failing test case

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-10-28 18:40:04 +01:00
Alejandro Alonso
4dc684dd9c Merge remote-tracking branch 'origin/staging' into develop 2026-10-28 11:50:56 +01:00
Pablo Alba
0532f15cee
🐛 Fix tab on variant name doesn't validate formula (#12655)
* 🐛 Fix tab on variant name doesn't validate formula

* ✨ Move helpers to common

* ✨ Mr proposed changes
2026-10-28 11:44:38 +01:00
Alejandro Alonso
a8e8a54a2b Merge remote-tracking branch 'origin/staging' into develop 2026-10-28 10:35:31 +01:00
Pablo Alba
f1d15aee4b
✨ Keep toast for file reparation open until user closes it (#12662) 2026-10-27 19:47:10 +01:00
Alonso Torres
a70712fce4
🐛 Fix issue with compononent not sync on move (#11981) 2026-10-27 15:44:30 +01:00
Alejandro Alonso
deb5771248
✨ Use WASM for SVG markup in code inspect and copy (#12567) (#12608)
When render-wasm is active, generate SVG for the Code inspector and
Copy as SVG from the WASM export path instead of classic SVG codegen.
Add multi-root render_shapes_svg so multi-select stays one document.

Closes #12567
2026-10-27 12:05:30 +01:00
Eva Marco
86e01d56a2
🐛 Update canvas and keep local sets when switching tokens source (#12442)
Setting a library as tokens source did not update shapes that use tokens,
because set-tokens-source never ran the token propagation that every other
token operation runs. Run it after the commit.

Going back to the local tokens, or unlinking the library that was the
source, left no local set active: the file has a single tokens status, and
switching to a library replaced it with the library's one. Keep the status
of the file's own tokens lib aside in a new optional :own-tokens-status
attribute while an external library is the source, and restore it when the
file is its own source again. Exporting without libraries and absorbing a
deleted source library restore it as well.

Closes #12430

AI-assisted-by: claude-opus-5-5
2026-10-26 15:23:05 +01:00
Belén Albeza
577cbe43e4
🐛 Validate the text payload before pasting it (#12612)
Text copied in Penpot carries its content in a data attribute of the
clipboard HTML, which plugins and other apps can rewrite. The v3 editor
and canvas paste used it as is, so bad data could reach WASM and the
file and break the shape.

The payload is now dropped, and the paste falls back to plain text in
the caret style, unless its content passes the text shape schema, its
span geometry holds only safe numbers and its file and team ids are
uuids. Payloads in HTML over the paste size limit are not decoded.

Copied text shapes pasted into the editor get the same checks, plus a
numeric position; when they fail, nothing is pasted.

Closes #12167

AI-assisted-by: claude-opus-5-5
2026-10-26 12:36:13 +01:00
Alonso Torres
4fe73011ae
🐛 Reject restoring components of a linked library from plugins (#12611)
LibraryComponent.restore() did not check that the component belongs to
the current file's library. Called on a deleted component of a connected
shared library, it restored that component and saved the change to the
library file, while remove(), annotation and duplicate() reject such
handles with a validation error.

restore() now rejects them the same way: "Can only restore components
from the current file's library".

Closes #12607
2026-10-26 12:21:58 +01:00
Eva Marco
68ecfb1234
🐛 Fix default shortcuts that fail on some layouts and browsers (#12506)
Fixes the problems found after #12326 and listed in #12327.

Mousetrap now tries several names for each key press, in order, and
fires the first one with a binding: letters and digits by keyCode when
a modifier is held, punctuation by position (layout map, or a US table
when the browser has none), the typed glyph, and the typed glyph
without the Shift it needed. With AltGr the typed glyph comes before
the position. The shortcut recorder asks Mousetrap for the key it
stores, so a recorded shortcut always fires.

This makes "?" and "+" work on every layout, Shift and Ctrl+Shift with
punctuation work in Firefox and Safari, Option+letter work on macOS,
and AltGr+º hide the UI on Linux with a Spanish layout.

Chrome on Linux returns the map of the first configured layout, not
the active one. Mousetrap now drops the map when an unmodified
punctuation key types something else.

The layout twins are no longer needed and are removed: "shift+'" made
"?" zoom to the selection on Spanish layouts. The Add node shortcut is
removed too: on US layouts its key is the "+" zoom key, and on Firefox
with a Spanish layout it cannot fire. The Firefox and Safari warning
in the shortcuts settings is gone, since those shortcuts now work
there. The user guide and translations follow these changes.

AI-assisted-by: claude-opus-5-5
2026-10-26 11:56:09 +01:00
Pablo Alba
3cd073fc6a
✨ Add auto-repair for integrity error files (#12524)
* ✨ Add auto-repair for integrity error files

* ✨ MR changes
2026-10-26 11:04:06 +01:00
Alejandro Alonso
e764b17c31 Merge remote-tracking branch 'origin/staging' into develop 2026-10-26 09:53:59 +01:00
Andrey Antukh
74add05226 Merge remote-tracking branch 'origin/staging' into develop 2026-10-23 13:40:44 +02:00
Luis de Dios
4bf70de73c
🐛 Restrict per-side stroke width tokens to boards and rects in plugin API (#12332)
* 🐛 Fix restrict per-side stroke width tokens to rectangles in plugins API

* ♻️ Reuse per-side stroke restriction check across plugin API

Move the per-side stroke width restriction into the public
`per-side-stroke-attrs-restricted?` helper, now taking `page-id`, and reuse
it from both `shape.applyToken` and the token application path. Keep the
error message in a single `per-side-stroke-widths-message` def used by all
three rejection sites.

Wait for token resolution in the uniform stroke width regression test
instead of relying on a bare `settle`, which could assert before the
applied token lands.

AI-assisted-by: deepseek-v4.1-flash

---------

Co-authored-by: Luis de Dios <luis.dedios@kaieldos.net>
2026-10-23 13:32:39 +02:00
Belén Albeza
2045fc4900
🐛 Keep text image fills in place when pasting (#12552)
A text image fill was always laid over the whole text box, so a pasted
image span showed another part of the picture in a box of another size.

Text image fills now honour the image transform, as shape fills do: the
image covers the transform rect instead of the whole box. Paste gives
each pasted image span a transform that keeps the image at the same
pixel offset from the span, like the gradients.

The SVG export emits one linked image per span transform, rather than
one per image id, and the style panel keeps the transform it reads back.

AI-assisted-by: claude-opus-5-5
2026-10-23 13:00:53 +02:00
Alejandro Alonso
e27c65c77a
✨ Run WASM asset exports locally in the browser (#12515)
When render-wasm is active, shape exports (single file, ZIP, and Export
boards multi-page PDF) run on the shared worker WASM module so the UI
stays responsive. A progressive PDF session (begin/add-page/finish)
updates progress per page; cancel settles on the client. The server
exporter path remains for the SVG renderer.
2026-10-23 11:02:23 +02:00
Florian Schrödl
9aa78a536b
✨ Resolve tokens with the tokenscript-interpreter package (#12348)
Replace the @penpot/tokenscript npm package (the TypeScript
interpreter) with penpot/tokenscript-interpreter, a Clojure and
ClojureScript interpreter that now lives in its own repository and
compiles into the frontend build.

The frontend adapter (app.main.data.tokenscript) resolves tokens with
it behind the existing :tokenscript flag and maps its results to the
values and errors the workspace uses:

- Token values are validated by the interpreter's token types, passed
  by their DTCG names: `1foo` is not a dimension, a number has no
  unit, an opacity is between 0 and 1. Penpot only adds its own
  limits (font weight names, text decoration keywords).
- Division by zero, incompatible units, unknown functions and
  function arguments out of range get messages of their own, and
  token pills show the message of their error.
- Font family names are quoted before resolving, as in the adapter
  this replaces (#11750): `Red Hat` stays one name.

The token form's value input passed the token name to the tokenscript
resolver in place of the typed value, and no name at all for a new
token. It now resolves the typed value under the form's token name, as
the StyleDictionary path does.

StyleDictionary stays the default until the interpreter is stable.

frontend/deps.edn pins the interpreter by tag and commit, v0.1.0.

Signed-off-by: Florian Schroedl <flo.schroedl@gmail.com>
2026-10-22 17:32:52 +02:00
Belén Albeza
0505eb3b1d
🐛 Fix pasted gradient fills in text editor v3 (#12516)
* 🐛 Keep text gradients in place when pasting at the caret

Text gradients are stored as fractions of the whole text box. Paste
copied those fractions unchanged, so in a box of another size the
handles and colours moved away from the pasted text.

Copy from the v3 editor now records where each span with a gradient or
image fill starts in its text box, and the box size, in the clipboard
payload. A new WASM export, text_editor_get_range_origin, gives the
start of any text range.

Paste at the caret finds where those spans start in the target and
rewrites their gradients so the handles keep the same pixel offset from
the span. Auto-sized texts use the box size they get when the editor
closes.

AI-assisted-by: claude-opus-5-5

* 🐛 Rebuild text gradient shaders when the text box resizes

Text gradient and image shaders are built into the cached Skia
paragraphs against the text box size. A resize with no content change
reused those paragraphs, so the colours stayed matched to the old box
until a reload.

The text now remembers the box size its shaders were built for and lays
itself out again when that size changes and a span has a gradient or
image fill. Text with only solid fills keeps its cache.

AI-assisted-by: claude-opus-5-5

* 🐛 Keep text gradients in place when pasting on the canvas

A text selection pasted on the canvas becomes a new text whose box
differs from the source, so its gradients moved like they did for caret
paste.

After adding the new text, lay it out, find where its gradient spans
start and move their gradients to keep the same pixel offset from the
span, in the same undo step. The range origin export now takes a shape
id, so it works on texts that are not being edited.

AI-assisted-by: claude-opus-5-5

* 🐛 Keep text gradients in place when pasting copied texts at the caret

Text shapes copied whole, rather than as an editor selection, reached the
caret paste without any geometry, so their gradients still moved away from
the pasted text.

A shape copy now records where the gradient and image spans of each
selected text start, and the box size, by shape id. Paste joins them in
the order of the joined paragraphs, with no origin for texts copied
without one.

Copied texts each have their own box, so every span origin now carries
its own box size instead of one box for the whole payload.

AI-assisted-by: claude-opus-5-5
2026-10-22 13:39:26 +02:00
Eva Marco
ce5276065e
🐛 Skip unknown entries and reserve keys in shortcut settings (#12473)
* 🐛 Skip unknown entries when importing shortcuts

The shortcuts import rejected the whole file when it held a single entry
it did not know, so a profile exported from an older Penpot version, or
edited by hand, could not be imported at all. The spec in #9924 asks to
skip unknown keys, tell the user which ones, and apply the rest.

Importing now keeps the entries that map to a known shortcut with a valid
key combination and skips the rest: unknown contexts, unknown shortcut
keys, and commands that are not a key combination. The import modal
lists the skipped entries. The file is only rejected when it is not a
map or holds nothing that can be applied.

A context whose entries are all skipped is left out: an empty context
clears the user's shortcuts on import, so keeping it would wipe them.

valid-command? reads a command the way mousetrap does, so it accepts
every default shortcut and rejects strings mousetrap could never match.

AI-assisted-by: claude-opus-5-5

* 📎 Sort requires in shortcuts settings test

cljfmt expects the namespace requires in alphabetical order.

AI-assisted-by: claude-opus-5-5

* ✨ Reserve cancel, copy and paste shortcuts

Cancel, copy and paste are no longer customizable, in the workspace and
in the path editor, which shares their shortcut keys. Their key
combinations (Escape, Ctrl/Cmd+C, Ctrl/Cmd+V) are now reserved: the
shortcut recorder warns and blocks saving one of them for another
action, and the import skips entries that use them.

Reserved combinations come from the non-customizable workspace
shortcuts, so marking another one as non-customizable reserves its key
too. The path editor's cancel also binds V, the move tool key, so path
shortcuts are left out. Combinations compare the way mousetrap matches
them: aliases and modifier order do not matter.

apply-custom-overrides now ignores overrides of non-customizable
shortcuts. An override saved before this change would otherwise still
apply, with no way to reset it from the settings page.

AI-assisted-by: claude-opus-5-5
2026-10-22 12:24:12 +02:00
María Valderrama
338d009c1e
🐛 Fix sorting for accented teams and organizations (#12505)
* 🐛 Fix sorting for accented teams and organizations

* 📎 Code review
2026-10-21 15:12:41 +02:00
Andrés Moya
3863e92993
🐛 Fix loss of swap slot when copying to clipboard (#12434)
* 🐛 Fix loss of swap slot when copying to clipboard

* 🔧 Add test and make test helpers run same code as real functions

* 🔧 Changes after review
2026-10-21 10:35:40 +02:00
Andrey Antukh
fcf9c27635 Merge remote-tracking branch 'origin/staging' into develop 2026-10-21 10:14:55 +02:00
Shreyash Agare
ecfa350470
✨ Auto-scroll sortable lists while dragging near their edges (#11595)
* ✨ Auto-scroll sortable lists while dragging near their edges

Native drag and drop swallows wheel events, so a long list could only be
scrolled mid-drag by hitting the browser's narrow auto-scroll strip at the
very edge of the container.

use-sortable now scrolls the item's nearest scrollable ancestor on every
animation frame while the pointer sits within 56px of its top or bottom
edge, ramping the speed up to 18px per frame as the pointer gets closer.
Every list built on the hook (layers, sitemap, option rows, gradients,
token sets) gets it without any change on their side.

* ✨ Cap auto-scroll zones and test the scroll loop

Each edge zone now takes at most a quarter of the container, so the
middle half of a short list, like the pages panel, never scrolls.

The scroll container is found once when the drag starts. The speed is
scaled by the time between frames, so 120 Hz screens no longer scroll
twice as fast. Starting a drag also stops any loop left over from a
drag whose end event was lost.

auto-scroll-speed is now private. New tests cover the running loop:
only one loop starts, it stops on drag end, when the speed goes back
to zero and after 500ms without dragover, and the frame time scaling.

AI-assisted-by: claude-opus-5-5

---------

Co-authored-by: Shreyash Agare <agareshreyash26@gmail.com>
Co-authored-by: Shreyash Agare <264953665+ShreyashAgare26@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-10-20 11:27:31 +02:00
Andrey Antukh
4635eaa467
🐛 Show request access modal on viewer and dashboard urls (#12456)
The exception page compared the screen query param against literals
that never occur ("view", "dashboard"), so viewer and dashboard urls
of inaccessible files and teams fell through to the plain not-found
page instead of the request access modal. Classify the param with a
tested helper that recognizes "viewer" and every "dashboard-*" route.

Closes #11224

AI-assisted-by: muse-spark-1.3-contributor
2026-10-19 22:33:10 +02:00