Feature/Discussion: CEP migration to UXP - #88
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a proof-of-concept Adobe UXP implementation of the AYON Photoshop panel, mirroring the existing CEP panel’s RPC-driven workflow while providing a dev-focused scaffold and documentation for evaluating a CEP→UXP migration.
Changes:
- Introduces a UXP panel implementation (
extension_uxp) with manifest, UI, websocket RPC client, and Photoshop action APIs. - Adds a development-oriented UXP folder (
extension_uxp_develop) with docs/notes and minimal Node type dependencies. - Updates the root README to point to the UXP fork/testing documentation.
Reviewed changes
Copilot reviewed 17 out of 25 changed files in this pull request and generated 24 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Links to the UXP fork/development README. |
| client/ayon_photoshop/api/extension_uxp/manifest.json | Adds UXP plugin manifest (host/version/permissions). |
| client/ayon_photoshop/api/extension_uxp/main.js | Panel bootstrap: connects RPC and binds UI buttons to routes. |
| client/ayon_photoshop/api/extension_uxp/LICENSE | Adds license file for the UXP subcomponent. |
| client/ayon_photoshop/api/extension_uxp/lib/wsrpc.min.js | Bundles minified websocket RPC library for the panel. |
| client/ayon_photoshop/api/extension_uxp/lib/wsrpc.js | Bundles readable websocket RPC library for the panel. |
| client/ayon_photoshop/api/extension_uxp/index.html | Adds the UXP panel UI markup and styles. |
| client/ayon_photoshop/api/extension_uxp/client_RPC.js | Implements client-side RPC routing between server and UXP APIs. |
| client/ayon_photoshop/api/extension_uxp/client_api.js | Implements Photoshop operations via UXP APIs and batchPlay. |
| client/ayon_photoshop/api/extension_uxp_develop/README.md | Documents local development/testing setup and constraints for UXP. |
| client/ayon_photoshop/api/extension_uxp_develop/package.json | Adds dev dependency for Adobe UXP Photoshop typings. |
| client/ayon_photoshop/api/extension_uxp_develop/package-lock.json | Lockfile for the UXP dev dependencies. |
| client/ayon_photoshop/api/extension_uxp_develop/ayon_uxp/NOTES.MD | Additional migration notes and UXP-specific gotchas. |
| client/ayon_photoshop/api/extension_uxp_develop/ayon_uxp/manifest.json | Dev copy of the UXP manifest. |
| client/ayon_photoshop/api/extension_uxp_develop/ayon_uxp/main.js | Dev copy of the panel bootstrap. |
| client/ayon_photoshop/api/extension_uxp_develop/ayon_uxp/LICENSE | Dev copy of the license file. |
| client/ayon_photoshop/api/extension_uxp_develop/ayon_uxp/lib/wsrpc.min.js | Dev copy of the minified RPC library. |
| client/ayon_photoshop/api/extension_uxp_develop/ayon_uxp/lib/wsrpc.js | Dev copy of the readable RPC library. |
| client/ayon_photoshop/api/extension_uxp_develop/ayon_uxp/index.html | Dev copy of the panel UI. |
| client/ayon_photoshop/api/extension_uxp_develop/ayon_uxp/client_RPC.js | Dev copy of the RPC routing implementation. |
| client/ayon_photoshop/api/extension_uxp_develop/ayon_uxp/client_api.js | Dev copy of the Photoshop UXP API implementation. |
| client/ayon_photoshop/api/extension_uxp_develop/.gitignore | Ignores Node/dev artifacts for the UXP dev folder. |
Files not reviewed (3)
- client/ayon_photoshop/api/extension_uxp/lib/wsrpc.js: Language not supported
- client/ayon_photoshop/api/extension_uxp_develop/ayon_uxp/lib/wsrpc.js: Language not supported
- client/ayon_photoshop/api/extension_uxp_develop/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
I guess the UXP has to be "build" and the code in client is source code? If that's the case can be the plugin moved to root of the repository to not include it in final zip? |
| "network": { | ||
| "domains": [ | ||
| "all", | ||
| "ws://localhost:8101" // <- Modify 8101 here to allow port through. |
There was a problem hiding this comment.
Have you tried ws://localhost:* to support any port?
There was a problem hiding this comment.
Have you tried
ws://localhost:*to support any port?
I tried doing this, plugin fails. This is not a supported scheme within UXP it seems.
From what I understand, any uri scheme that's not http/https needs to be explicitly stated.
|
Hi @Sasbom I would like to test this one on MacOS. Had installed UXP developer tools, but stragling to proceed to next steps. Could you please add for me simple step by step guide? |
The UXP plugin indeed needs to be built and then installed. I still have to figure out how this process can be automated, since it's not as simple as placing it somewhere in a folder. I placed the 2 UXP folders in the place I did just out of convention, the entire CEP source code is in the I totally agree that the "extension_uxp_develop" folder can live outside, and we can get rid of the |
@jakubjezek001 Within the UXP developer tools, go to "add plugin"
go to this plugins
The plugin should now appear.
Next, we need to modify the manifest to reflect the current expected CEP plugin version, which can be found in
next, we need to make a slight modification to (The desired websocket URL can be found / changed in We need to make sure the URL that we connect to is explicitly set to the desired websocket URL from the UXP plugin, so in my case I do this:
I do not worry about the fallback port. I know that the port can be parsed out just fine. To test this, package the plugin as it is right now, make sure it is in a bundle, and launch photoshop
The commandline for ayon photoshop should remain in this state now instead of connecting to the UXP plugin: Now we can go to the UXP developer tools again and click "Load" or "Load & Watch" if you'd like to debug.
The plugin window should pop up, like this:
if you'd like to view logging messages, that can be done through the "debug" option in the UXP tools (only if in "Watching" state)
and it should pop up a "browser like" console log.
One thing to note is that this doesn't report errors. It does allow you to interact with the running code which can be convenient sometimes, e.g, if you want to get the RPC object to manually send messages around from within photoshop.
If I now click workfiles you should see the whole communication chain.
I hope this clears things up! |
jakubjezek001
left a comment
There was a problem hiding this comment.
tested and works at current stage on MacOS
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 25 changed files in this pull request and generated 11 comments.
Files not reviewed (3)
- client/ayon_photoshop/api/extension_uxp/lib/wsrpc.js: Language not supported
- client/ayon_photoshop/api/extension_uxp_develop/ayon_uxp/lib/wsrpc.js: Language not supported
- client/ayon_photoshop/api/extension_uxp_develop/package-lock.json: Language not supported
| async function setup_rpc(websocket_url) { | ||
| if (websocket_url) | ||
| console.log("websocket_url", websocket_url); | ||
|
|
||
| const default_url = 'ws://localhost:8101/ws/'; | ||
|
|
||
| if (websocket_url == ''){ | ||
| websocket_url = default_url; | ||
| } | ||
|
|
||
| RPC = new WSRPC(websocket_url); // spin connection | ||
| console.log("connecting to:", websocket_url, RPC); | ||
| try{ | ||
| RPC.connect(); | ||
| } catch (err) { | ||
| console.log(err) | ||
| } | ||
| // await RPC.onEvent("onconnect"); | ||
| console.log("Connected!"); |
There was a problem hiding this comment.
This is a highly unrealistic scenario for this code use. It will always get called with the string provided in main.js.
| // this route appears to not exist anymore. | ||
| RPC.call('Photoshop.ping').then(function (data) { |
There was a problem hiding this comment.
Alright, couldn't find the route last time but it's there.
| <div class="btn" id="workfiles-button">Workfiles...</div> | ||
| <div class="btn" id="loader-button">Load...</div> | ||
| <div class="btn" id="publish-button">Publish...</div> | ||
| <div class="btn" id="sceneinventory-button">Manage...</div> | ||
| <div class="btn" id="experimental-button">Experimental Tools...</div> |
| async function setup_rpc(websocket_url) { | ||
| if (websocket_url) | ||
| console.log("websocket_url", websocket_url); | ||
|
|
||
| const default_url = 'ws://localhost:8101/ws/'; | ||
|
|
||
| if (websocket_url == ''){ | ||
| websocket_url = default_url; | ||
| } | ||
|
|
||
| RPC = new WSRPC(websocket_url); // spin connection | ||
| console.log("connecting to:", websocket_url, RPC); | ||
| try{ | ||
| RPC.connect(); | ||
| } catch (err) { | ||
| console.log(err) | ||
| } | ||
| // await RPC.onEvent("onconnect"); | ||
| console.log("Connected!"); |
There was a problem hiding this comment.
Fringe case to be worried about erors.
see use in main.js
| <div class="btn" id="workfiles-button">Workfiles...</div> | ||
| <div class="btn" id="loader-button">Load...</div> | ||
| <div class="btn" id="publish-button">Publish...</div> | ||
| <div class="btn" id="sceneinventory-button">Manage...</div> | ||
| <div class="btn" id="experimental-button">Experimental Tools...</div> |
There was a problem hiding this comment.
Is done on purpose. We use divs to allow for styling, to break away from adobe's own look.
| // importing through structured binding from a requires statement | ||
| const {module1, module2} = requires("photoshop"); |
| Yet another quirk: **UXP Plugins can NOT read environment variables.** | ||
| This has led me to have to decide that this plugin will need to communicate over a fixed adress. | ||
|
|
| class WebServerTool: | ||
| """ | ||
| Basic POC implementation of asychronic websocket RPC server. | ||
| Uses class in external_app_1.py to mimic implementation for single |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
update notes with most of the suggested spelling fixes.
Probably both can be moved out. We just need the final build to be in the zip file. There is no reason to include source code for plugin in client zip if it has no usage on client machines. |
I think I'll keep only one, the develop code, since adobe doesn't package up much else anyway. Ill leave consolidation of docs to you guys, |
I don't understand, why to keep the source code in client directory if it is not used client side? That's just wasted upload/download/zip/unzip time. |
I'm sorry, maybe bad wording. First I had 2 folders. I chose to keep the development one with the lock file for npm. I moved that one to the root of the repository! The other folder, I removed it from the repo to prevent code repetition. |
| return RPC; | ||
| } | ||
|
|
||
| async function setup_rpc(websocket_url) { |
There was a problem hiding this comment.
Shouldn't this still look for the env variable and don't create the connection if is not set?
There was a problem hiding this comment.
Shouldn't this still look for the env variable and don't create the connection if is not set?
Here's the fun thing about UXP. You can not access environment variables...
UXP is much more "gated". You couldn't read a file either, unless it was packaged with the actual plugin (hence why we can still inspect the manifest), and "automating" the packaging and deployment of the plugin seems to not be possible.
I wrote instructions in the entry post that outlined this, as well as the modifications that need to be made to, in particular, api/webserver.py to make this plugin work.
I'd like to ask for some help deciding on what would be the most canonical, AYON-consistent way to include the UXP component within this plugin.
There was a problem hiding this comment.
You can not access environment variables...
Oh, that, makes sense? Ok.. That is kinda limiting. Then maybe something like this but only 2-3 ports?
| "network": { | ||
| "domains": [ | ||
| "all", | ||
| "ws://localhost:8101" |
There was a problem hiding this comment.
I think there was conversation if you tried ws://localhost:*. But have you tried IP address instead of localhost? ws://127.0.0.1:* and ws://[::1]:* (in case someone would default to IPv6).
There was a problem hiding this comment.
Also it might be good option to open this for more than just one port. We can define 10 possible ports, the same range would be tried in python.
"ws://localhost:8101",
"ws://localhost:8102",
"ws://localhost:8103",
"ws://localhost:8104",
"ws://localhost:8105",
"ws://localhost:8106",
"ws://localhost:8107",
"ws://localhost:8108",
"ws://localhost:8109",
"ws://localhost:8110"
There was a problem hiding this comment.
I'll test IPV6 tomorrow but I haven't had any success with globbing the port number.
I think your suggestion of defining a range of ports would be ideal,
and on the UXP side we'd need to do something like
const ws_urls = Array.from(new Array(10), (_, i) => 'ws://127.0.0.1:'+ (8101 + i) );
for (const ws_url in ws_urls) {
try {
establishRPCConnection(ws_url);
} catch (err) {
console.log("Failed for url", ws_url);
continue;
}
}or something to this extent if we end up going that route.
Unfortunatly we can't do some IPC trick of "write to file -> package -> read within UXP" because those tools can't be expected to be present on the user's PC nor do we know if they are automatable.
Oh wait... we can't reliably do this, I guess, we need to keep cycling on the UXP site, trying to ping those ports
On the python side where the server happens it should actually be decided on which port the server is going to be hosted, because the same port can't be served on twice of course, which is the whole reason you'd want to try a couple out in the first place.
The "risk" you'd then be willing to take is, should one of those ports be occupied already (the whole reason we are cycling anyway), should one port earlier in the list have an accepting websocket connection open, we'd then get into trouble because UXP, connecting as a client, would get stuck being connected but unable to call any methods.
To prevent this I guess you could try and ping the server with some handshake, but I wonder if that's making things too complicated. The issue is a broader range of ports in the first place, ideally you'd just define one, as we were able to do in the CEP plugin.
Hi, please consult this earlier message: The ping probably comes from the old extension still. You need to still "patch in" the alternative websocket port. The comment linked above describes how to do that. Sorry for the inconvenience, I really need some input on a neat interface for this as a major roadblock is the fact we can't pass around some port though IPC within the whole UXP framework. Thank you for taking time to test! |
|
Shii.. my bad, got lost in the guide. Reading about the issues with env vars, installation, debugging etc, I still don't feel that UXP is such great progress from old CEP, same way as I thought about it last time I checked it :D. Well, Adobe, as expected ;). |
it's... better, if you look at the code it's not that bad. Adobe makes official NPM stubs now (although, they aren't fully complete for photoshop at least so you better keep the docs to the side) and the tooling is overall more convenient and modern (stronger preference for async etc) at the loss of a loooot of injectability and general tomfoolery you can do. I'd call it, one step forward, 3/4 step back hahaha. Overall I like the style of code way more even though it's still tacky with having to essentially brute force macros for some things. The debugging is actually quite nice! (if you remember to properly wrap errorable stuff in try catch blocks....) |
|
Apparently, it's not impossible to install CCX plugins unsupervised. We can do this by utilizing the UPIA tool, Unified Plugin Installer Agent command-line documentation I just stumbled into this today, excusez-moi. |
I tested that, it said that it installed it fine, but plugin wasn't working (it didn't connect to webserver). Only after I loaded plugin via Otherwise it is working afterwards fine. I think we still need to figure out, how to install that plugin. It forced me to load it any time PS was restarted. That is not viable option for artists. Questions for @BigRoy:
|
This was causing bugs because in runtime, the class 'LayerKind' etc. can't be found. Constant properties like "LayerKind.GROUP" must be accessed through constants.ClassName.CONSTANTNAME
|
Just a note, upon further use and abuse, there were some inconsistencies in behaviors that I've since hammered out. |
|
How are you actually deploying it in production? I failed to install it via command line, manual install was required, which seems as a tall order for artists. |
We manually deploy it on the computers that need it, unfortunatly. We can still manage because usually the people that need it are few and far between. There's also a different option apparently? Adobe "Developer distribution portal", documented here, It seems like accounts bundled under a shared organization can share a plugin this way, but I don't manage our accounts so I know very little. I really dislike how inconvenient things have become, which to a certain extent is understandable, but for it to be so gated behind explicit enterprise bars annoys me to no end. |














Changelog Description
This PR is meant as a proof of concept / demo of what a UXP implementation of the internal plugin can look like.
It works (somewhat) exactly like the CEP plugin, with some limitations that come with the UXP environment being more sandboxed, but otherwise plugin functionality isn't compromised and all hooks are implemented.
This PR is explicitly meant to open up a discussion on what a workflow could look like with UXP.
Maybe we'll have to tackle this through a hybridized version where both CEP and UXP are available in different contexts (e.g, headless operations would still need CEP)
Additional review information
Detailed explanations of what exactly is going on in the move to UXP, refer Here
Testing notes:
Please note that automatic injection of the UXP plugin isn't functionally present yet.
I have provided notes on what to change to manually test this out, as it's still a draft.
Refer to Here
For information on testing the plugin.
The plugin has been tested internally on both MacOSX (on a M-series chipset) and Windows both.
This is a big advantage of UXP, it works on more platforms, it's still in support and not on it's way to deprecation,
and it's generally a lot cleaner to program with, even though it's still an older dialect of CommonJS.
Thanks for your consideration,
Sas van Gulik @ Submarine.