diff --git a/.opencode/skills/create-pr/SKILL.md b/.opencode/skills/create-pr/SKILL.md new file mode 100644 index 0000000000..88b5464481 --- /dev/null +++ b/.opencode/skills/create-pr/SKILL.md @@ -0,0 +1,81 @@ +--- +name: create-pr +description: Create a GitHub PR following Penpot conventions, with a concise engineer-focused description +--- + +# Create Pull Request + +Create a GitHub PR with proper title format and a concise description that explains reasoning, not implementation details. + +## When to Use + +- Opening a new pull request +- The user asks to create a PR +- Code changes are ready and committed + +## Workflow + +### 1. Verify Prerequisites + +```bash +git branch --show-current +git log --oneline main..HEAD +``` + +### 2. Check if Branch is Pushed + +```bash +BRANCH=$(git branch --show-current) +if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then + echo "Branch is pushed, proceeding with PR creation" +else + echo "ERROR: Branch '$BRANCH' is not pushed to remote. Please push the branch first." + exit 1 +fi +``` + +**If the branch is not pushed, STOP here and ask the user to push it. The LLM does not have push permissions.** + +### 3. Create PR Body + +Write to `/tmp/pr-body.md` to avoid shell quoting issues: + +```bash +cat > /tmp/pr-body.md << 'EOF' +**Note:** This PR was created with AI assistance. + +## What + + + +## Why + + + +## How + + +EOF +``` + +### 4. Create the PR + +Follow title and description format from `mem:workflow/creating-prs` and `mem:workflow/creating-commits`. + +```bash +gh pr create --base main --project "Main" --title "" --body-file /tmp/pr-body.md +``` + +### 5. What NOT to Include + +- ❌ List of files changed (visible in diff) +- ❌ Testing steps (CI handles this) +- ❌ Screenshots unless UI-visible +- ❌ Migration notes unless breaking changes +- ❌ Regression fixes introduced during the PR (they're part of the development process, not the feature) + +## Key Principles + +- **Write for humans.** The diff shows what changed. The description explains why. +- **Be concise.** Focus on reasoning: What was the problem? Why did it happen? How did you solve it? +- **Skip the obvious.** Don't explain what `git diff` already shows. diff --git a/frontend/src/app/main/data/plugins.cljs b/frontend/src/app/main/data/plugins.cljs index 2ed86cf968..77f5d55013 100644 --- a/frontend/src/app/main/data/plugins.cljs +++ b/frontend/src/app/main/data/plugins.cljs @@ -9,6 +9,7 @@ [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.files.changes-builder :as pcb] + [app.common.logging :as log] [app.common.time :as ct] [app.main.data.changes :as dch] [app.main.data.event :as ev] @@ -57,45 +58,57 @@ (defn start-plugin! [{:keys [plugin-id name version description host code permissions allow-background]} ^js extensions] - (-> (.ɵloadPlugin - ^js ug/global - #js {:pluginId plugin-id - :name name - :version version - :description description - :host host - :code code - :allowBackground (boolean allow-background) - :permissions (apply array permissions)} - nil - extensions) + (let [load-plugin (unchecked-get ug/global "ɵloadPlugin")] + (if (fn? load-plugin) + (-> (load-plugin + #js {:pluginId plugin-id + :name name + :version version + :description description + :host host + :code code + :allowBackground (boolean allow-background) + :permissions (apply array permissions)} + nil + extensions) - (p/catch (fn [cause] - (ex/print-throwable cause :prefix "Plugin Error") - (errors/flash :cause cause :type :handled))))) + (p/catch (fn [cause] + (ex/print-throwable cause :prefix "Plugin Error") + (errors/flash :cause cause :type :handled)))) + + (log/warn :hint "Plugin runtime not initialized yet" + :plugin-id plugin-id + :action "start-plugin!")))) (defn- load-plugin! [{:keys [plugin-id name version description host code icon permissions]}] (st/emit! (pflag/clear plugin-id) (save-current-plugin plugin-id)) - (-> (.ɵloadPlugin - ^js ug/global - #js {:pluginId plugin-id - :name name - :description description - :version version - :host host - :code code - :icon icon - :permissions (apply array permissions)} - (fn [] - (st/emit! (remove-current-plugin plugin-id)))) + (let [load-plugin (unchecked-get ug/global "ɵloadPlugin")] + (if (fn? load-plugin) + (-> (load-plugin + #js {:pluginId plugin-id + :name name + :description description + :version version + :host host + :code code + :icon icon + :permissions (apply array permissions)} + (fn [] + (st/emit! (remove-current-plugin plugin-id)))) - (p/catch (fn [cause] - (st/emit! (remove-current-plugin plugin-id)) - (ex/print-throwable cause :prefix "Plugin Error") - (errors/flash :cause cause :type :handled))))) + (p/catch (fn [cause] + (st/emit! (remove-current-plugin plugin-id)) + (ex/print-throwable cause :prefix "Plugin Error") + (errors/flash :cause cause :type :handled)))) + + (do + (log/warn :hint "Plugin runtime not initialized yet" + :plugin-id plugin-id + :action "load-plugin!") + (st/emit! (remove-current-plugin plugin-id)))))) (defn open-plugin! [{:keys [url] :as manifest} user-can-edit?] @@ -135,10 +148,15 @@ (defn close-plugin! [{:keys [plugin-id]}] - (try - (.ɵunloadPlugin ^js ug/global plugin-id) - (catch :default e - (.error js/console "Error" e)))) + (let [unload-plugin (unchecked-get ug/global "ɵunloadPlugin")] + (if (fn? unload-plugin) + (try + (unload-plugin plugin-id) + (catch :default e + (.error js/console "Error" e))) + (log/warn :hint "Plugin runtime not initialized yet" + :plugin-id plugin-id + :action "close-plugin!")))) (defn close-current-plugin [& {:keys [close-only-edition-plugins?]}] diff --git a/frontend/src/app/main/data/workspace/mcp.cljs b/frontend/src/app/main/data/workspace/mcp.cljs index 7f0b0e3363..61e1116b49 100644 --- a/frontend/src/app/main/data/workspace/mcp.cljs +++ b/frontend/src/app/main/data/workspace/mcp.cljs @@ -15,7 +15,7 @@ [app.main.data.plugins :as dp] [app.main.repo :as rp] [app.main.store :as st] - [app.plugins.register :refer [mcp-plugin-id]] + [app.plugins.register :as preg] [app.util.i18n :refer [tr]] [app.util.timers :as ts] [beicon.v2.core :as rx] @@ -29,7 +29,7 @@ {:code "plugin.js" :name "Penpot MCP Plugin" :version 2 - :plugin-id mcp-plugin-id + :plugin-id preg/mcp-plugin-id :description "This plugin enables interaction with the Penpot MCP server" :allow-background true :permissions @@ -195,41 +195,45 @@ (defn init-mcp [stream] - (->> (rp/cmd! :get-current-mcp-token) - (rx/tap - (fn [{:keys [token]}] - (when token - (dp/start-plugin! - (assoc default-manifest - :url (str (u/join cf/public-uri "plugins/mcp/manifest.json")) - :host (str (u/join cf/public-uri "plugins/mcp/"))) + ;; Wait for plugins runtime to be initialized before starting the MCP plugin. + ;; This ensures global.ɵloadPlugin is available when start-plugin! is called. + (->> (rx/from (preg/wait-for-runtime)) + (rx/mapcat + (fn [_] + (->> (rp/cmd! :get-current-mcp-token) + (rx/tap + (fn [{:keys [token]}] + (when token + (dp/start-plugin! + (assoc default-manifest + :url (str (u/join cf/public-uri "plugins/mcp/manifest.json")) + :host (str (u/join cf/public-uri "plugins/mcp/"))) - ;; API extension for MCP server - #js {:mcp - #js - {:getToken (constantly token) - :getServerUrl #(str cf/mcp-ws-uri) - :setMcpStatus - (fn [status] - (when (= status "connected") - (start-reconnect-watcher!)) - (st/emit! (update-mcp-connection-status status)) - (log/info :hint "MCP STATUS" :status status)) + ;; API extension for MCP server + #js {:mcp + #js + {:getToken (constantly token) + :getServerUrl #(str cf/mcp-ws-uri) + :setMcpStatus + (fn [status] + (when (= status "connected") + (start-reconnect-watcher!)) + (st/emit! (update-mcp-connection-status status)) + (log/info :hint "MCP STATUS" :status status)) - :on - (fn [event cb] - (when-let [event - (case event - "disconnect" ::disconnect - "connect" ::connect - nil)] + :on + (fn [event cb] + (when-let [event + (case event + "disconnect" ::disconnect + "connect" ::connect + nil)] - (let [stopper (rx/filter finalize-workspace? stream)] - (->> stream - (rx/filter (ptk/type? event)) - (rx/take-until stopper) - (rx/subs! #(cb))))))}})))) - (rx/ignore))) + (let [stopper (rx/filter finalize-workspace? stream)] + (->> stream + (rx/filter (ptk/type? event)) + (rx/take-until stopper) + (rx/subs! #(cb))))))}}))))))))) (defn init [] diff --git a/frontend/src/app/plugins.cljs b/frontend/src/app/plugins.cljs index 779c09b9fd..8937b4dde6 100644 --- a/frontend/src/app/plugins.cljs +++ b/frontend/src/app/plugins.cljs @@ -17,14 +17,17 @@ [app.plugins.grid :as grid] [app.plugins.library :as library] [app.plugins.public-utils] + [app.plugins.register :as preg] [app.plugins.ruler-guides :as rg] [app.plugins.shape :as shape] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) -(defn init-plugins-runtime! +(defn init-plugins-runtime [] - (runtime/initPluginsRuntime (fn [plugin-id] (api/create-context plugin-id)))) + (runtime/initPluginsRuntime (fn [plugin-id] (api/create-context plugin-id))) + ;; Signal that runtime is ready + (preg/signal-runtime-ready)) (defn initialize [] @@ -38,7 +41,7 @@ (rx/observe-on :async) (rx/filter #(features/active-feature? @st/state "plugins/runtime")) (rx/take 1) - (rx/tap init-plugins-runtime!) + (rx/tap init-plugins-runtime) (rx/ignore))))) ;; Prevent circular dependency diff --git a/frontend/src/app/plugins/register.cljs b/frontend/src/app/plugins/register.cljs index d05b08d547..e4837ce75b 100644 --- a/frontend/src/app/plugins/register.cljs +++ b/frontend/src/app/plugins/register.cljs @@ -15,12 +15,28 @@ [app.main.repo :as rp] [app.main.store :as st] [app.util.object :as obj] - [beicon.v2.core :as rx])) + [beicon.v2.core :as rx] + [promesa.core :as p])) ;; Needs to be here because moving it to `app.main.data.workspace.mcp` will ;; cause a circular dependency (def mcp-plugin-id "96dfa740-005d-8020-8007-55ede24a2bae") +;; Promise that resolves when plugins runtime is initialized. +;; Lives here to avoid circular dependency: workspace.mcp -> app.plugins -> app.plugins.api -> workspace +(defonce ^:private runtime-ready-promise (p/deferred)) + +(defn wait-for-runtime + "Returns a promise that resolves when plugins runtime is initialized." + [] + runtime-ready-promise) + +(defn signal-runtime-ready + "Signals that plugins runtime has been initialized. Called by app.plugins/init-plugins-runtime." + [] + (when (p/pending? runtime-ready-promise) + (p/resolve! runtime-ready-promise true))) + ;; Stores the installed plugins information (defonce ^:private registry (atom {})) diff --git a/frontend/src/features.cljs b/frontend/src/features.cljs index 77f15664ba..be61548a1b 100644 --- a/frontend/src/features.cljs +++ b/frontend/src/features.cljs @@ -24,5 +24,5 @@ (defn ^:export plugins [] (st/emit! (features/enable-feature "plugins/runtime")) - (plugins/init-plugins-runtime!) + (plugins/init-plugins-runtime) nil)