-
Notifications
You must be signed in to change notification settings - Fork 0
Fix DXT download button: use figma.openExternal + Copy-link fallback #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5ec4ded
cdca80e
cc3fade
d5979b9
aaafefa
db095cc
c4c5fab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import { describe, it, expect, vi } from "vitest"; | ||
| import { handleOpenExternal } from "../handlers/open-external"; | ||
|
|
||
| describe("handleOpenExternal", () => { | ||
| it("calls figma.openExternal with the url when type is 'open-external' and url is a string", () => { | ||
| const openExternal = vi.fn(); | ||
| const figmaRef = { openExternal } as unknown as PluginAPI; | ||
| const dispatched = handleOpenExternal( | ||
| { type: "open-external", url: "https://example.com/pluginos.dxt" }, | ||
| figmaRef | ||
| ); | ||
| expect(dispatched).toBe(true); | ||
| expect(openExternal).toHaveBeenCalledTimes(1); | ||
| expect(openExternal).toHaveBeenCalledWith("https://example.com/pluginos.dxt"); | ||
| }); | ||
|
|
||
| it("does NOT call figma.openExternal when url is missing", () => { | ||
| const openExternal = vi.fn(); | ||
| const figmaRef = { openExternal } as unknown as PluginAPI; | ||
| const dispatched = handleOpenExternal({ type: "open-external" }, figmaRef); | ||
| expect(dispatched).toBe(false); | ||
| expect(openExternal).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("does NOT call figma.openExternal when url is not a string", () => { | ||
| const openExternal = vi.fn(); | ||
| const figmaRef = { openExternal } as unknown as PluginAPI; | ||
| const dispatched = handleOpenExternal({ type: "open-external", url: 42 }, figmaRef); | ||
| expect(dispatched).toBe(false); | ||
| expect(openExternal).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("does NOT call figma.openExternal when url is an empty string", () => { | ||
| const openExternal = vi.fn(); | ||
| const figmaRef = { openExternal } as unknown as PluginAPI; | ||
| const dispatched = handleOpenExternal({ type: "open-external", url: "" }, figmaRef); | ||
| expect(dispatched).toBe(false); | ||
| expect(openExternal).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("does NOT call figma.openExternal when url is null", () => { | ||
| const openExternal = vi.fn(); | ||
| const figmaRef = { openExternal } as unknown as PluginAPI; | ||
| const dispatched = handleOpenExternal({ type: "open-external", url: null }, figmaRef); | ||
| expect(dispatched).toBe(false); | ||
| expect(openExternal).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("returns false for unrelated message types", () => { | ||
| const openExternal = vi.fn(); | ||
| const figmaRef = { openExternal } as unknown as PluginAPI; | ||
| const dispatched = handleOpenExternal({ type: "ws-message", payload: {} }, figmaRef); | ||
| expect(dispatched).toBe(false); | ||
| expect(openExternal).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -63,7 +63,8 @@ | |
| <div class="card-title">Claude Desktop</div> | ||
| <div class="card-desc">One-click install. Download the PluginOS extension for Claude Desktop, double-click to install, then return here. No terminal, no JSON.</div> | ||
| <div class="card-actions"> | ||
| <a class="btn btn-primary" id="btn-download-dxt" href="https://github.com/LSDimi/pluginos/releases/latest/download/pluginos.dxt" download>Download for Claude Desktop</a> | ||
| <button class="btn btn-primary" id="btn-download-dxt">Download for Claude Desktop</button> | ||
| <button class="btn" id="btn-copy-dxt-url">Copy link</button> | ||
| </div> | ||
| </div> | ||
|
|
||
|
|
@@ -132,6 +133,7 @@ | |
| var MCP_CONFIG_JSON = JSON.stringify({ mcpServers: { pluginos: { command: "npx", args: ["-y", "pluginos@0.4.1"] } } }, null, 2); | ||
| var TIER_1_RULES = "When working with Figma, always use PluginOS tools exclusively:\n- Use `list_operations` (pluginos) first to discover available Figma operations.\n- Use `run_operation` (pluginos) to execute them.\n- Use `execute_figma` (pluginos) only for one-off custom logic not covered by built-in ops.\n- Do NOT use `mcp__Figma__*` tools — they bypass the plugin and return raw, token-heavy data. PluginOS returns pre-summarized, structured results at ~230 tokens/call.\n- If PluginOS returns \"No plugin connected\", open the PluginOS Bridge plugin in Figma before retrying.\n\nAudit/lint/check operations default to `scope: \"selection\"`. Pass `scope: \"page\"` explicitly (and `confirm: true` for pages over 500 nodes) to scan the whole page. Responses carry `_hint` and `_next_hints` fields — respect them when deciding what to do next."; | ||
| var INSTALL_COMMAND = "/plugin marketplace add github:LSDimi/pluginos\n/plugin install pluginos"; | ||
| var DXT_URL = "https://github.com/LSDimi/pluginos/releases/latest/download/pluginos.dxt"; | ||
|
|
||
| function wireCopy(btnId, text) { | ||
| var btn = document.getElementById(btnId); | ||
|
|
@@ -160,9 +162,27 @@ | |
| } else { fallback(); } | ||
| }); | ||
| } | ||
| (function wireDownloadDxt() { | ||
| var btn = document.getElementById("btn-download-dxt"); | ||
| if (!btn) return; | ||
| btn.addEventListener("click", function() { | ||
| parent.postMessage( | ||
| { pluginMessage: { type: "open-external", url: DXT_URL } }, | ||
| "*" | ||
| ); | ||
| btn.classList.add("copied"); | ||
| var orig = btn.textContent; | ||
| btn.textContent = "✓ Opening in browser…"; | ||
| setTimeout(function() { | ||
| btn.textContent = orig; | ||
| btn.classList.remove("copied"); | ||
| }, 2500); | ||
|
Comment on lines
+173
to
+179
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic for updating the button text and class on click is duplicated from the |
||
| }); | ||
| })(); | ||
| wireCopy("btn-copy-install", INSTALL_COMMAND); | ||
| wireCopy("btn-copy-mcp-cursor", MCP_CONFIG_JSON); | ||
| wireCopy("btn-copy-rules-cursor", TIER_1_RULES); | ||
| wireCopy("btn-copy-dxt-url", DXT_URL); | ||
|
|
||
| function tryBootload() { | ||
| setDot("dot-yellow"); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| /** | ||
| * Handle `{ type: "open-external", url: string }` messages from the plugin UI. | ||
| * | ||
| * Why this exists: Figma renders plugin UIs in a sandboxed iframe. Clicking an | ||
| * <a href="..." download> navigates the iframe itself and blanks the plugin | ||
| * view — the `download` attribute is ignored. The Figma-standard way to open | ||
| * an external URL is `figma.openExternal(url)`, which opens the user's | ||
| * default browser without touching the iframe. | ||
| * | ||
| * Returns true when the message was dispatched, false otherwise — lets the | ||
| * caller chain handlers without re-checking `msg.type`. | ||
| */ | ||
| export function handleOpenExternal( | ||
| msg: { type?: string; url?: unknown; [key: string]: unknown }, | ||
| figmaRef: Pick<PluginAPI, "openExternal"> | ||
| ): boolean { | ||
| if (msg.type !== "open-external") return false; | ||
| if (typeof msg.url !== "string" || msg.url.length === 0) return false; | ||
| figmaRef.openExternal(msg.url); | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
DXT_URLconstant is duplicated here and inpackages/bridge-plugin/src/ui-entry.ts. This increases the risk of inconsistency if the URL needs to be updated in the future. Consider centralizing this value in a shared configuration or constants file that can be accessed by both the main UI and the bootloader.