From 884cdbbf8de760847ba3440859745c4696bf4890 Mon Sep 17 00:00:00 2001 From: Juan de la Cruz Date: Mon, 23 Mar 2026 11:20:37 +0100 Subject: [PATCH] :sparkles: Add new MCP plugin UI changes (#8699) * :sparkles: Add new MCP plugin UI changes * :paperclip: Fix tool status misleading --- mcp/packages/plugin/index.html | 77 +++++++++- mcp/packages/plugin/public/icon.jpg | Bin 0 -> 7632 bytes mcp/packages/plugin/public/manifest.json | 1 + mcp/packages/plugin/src/main.ts | 99 ++++++++++--- mcp/packages/plugin/src/plugin.ts | 4 +- mcp/packages/plugin/src/style.css | 176 ++++++++++++++++++++++- 6 files changed, 329 insertions(+), 28 deletions(-) create mode 100644 mcp/packages/plugin/public/icon.jpg diff --git a/mcp/packages/plugin/index.html b/mcp/packages/plugin/index.html index b2c08b5dae..fa573c1d0e 100644 --- a/mcp/packages/plugin/index.html +++ b/mcp/packages/plugin/index.html @@ -3,12 +3,83 @@ - Penpot plugin example + Penpot MCP Plugin - +
+
+ + Not connected +
-
Not connected
+ + + +
+ + + Execution status + + +
+ Current task +
+ + --- +
+ +
+ Executed code + +
+ +
+
+
diff --git a/mcp/packages/plugin/public/icon.jpg b/mcp/packages/plugin/public/icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9df8dd26a4a08fea0c10604f732f7ceb204b7bf4 GIT binary patch literal 7632 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 M+yAL{;b-Ij2S*HOvH$=8 literal 0 HcmV?d00001 diff --git a/mcp/packages/plugin/public/manifest.json b/mcp/packages/plugin/public/manifest.json index e2a769c7f8..aa97095b30 100644 --- a/mcp/packages/plugin/public/manifest.json +++ b/mcp/packages/plugin/public/manifest.json @@ -1,6 +1,7 @@ { "name": "Penpot MCP Plugin", "code": "plugin.js", + "icon": "icon.jpg", "version": 2, "description": "This plugin enables interaction with the Penpot MCP server", "permissions": ["content:read", "content:write", "library:read", "library:write", "comment:read", "comment:write"] diff --git a/mcp/packages/plugin/src/main.ts b/mcp/packages/plugin/src/main.ts index da87fa025f..8aad137ec3 100644 --- a/mcp/packages/plugin/src/main.ts +++ b/mcp/packages/plugin/src/main.ts @@ -6,21 +6,33 @@ document.body.dataset.theme = searchParams.get("theme") ?? "light"; // WebSocket connection management let ws: WebSocket | null = null; -const statusElement = document.getElementById("connection-status"); + +const statusPill = document.getElementById("connection-status") as HTMLElement; +const statusText = document.getElementById("status-text") as HTMLElement; +const currentTaskEl = document.getElementById("current-task") as HTMLElement; +const executedCodeEl = document.getElementById("executed-code") as HTMLTextAreaElement; +const copyCodeBtn = document.getElementById("copy-code-btn") as HTMLButtonElement; +const connectBtn = document.getElementById("connect-btn") as HTMLButtonElement; +const disconnectBtn = document.getElementById("disconnect-btn") as HTMLButtonElement; /** - * Updates the connection status display element. + * Updates the status pill and button visibility based on connection state. * - * @param status - the base status text to display - * @param isConnectedState - whether the connection is in a connected state (affects color) - * @param message - optional additional message to append to the status + * @param code - the connection state code ("idle" | "connecting" | "connected" | "disconnected" | "error") + * @param label - human-readable label to display inside the pill */ -function updateConnectionStatus(code: string, status: string, isConnectedState: boolean, message?: string): void { - if (statusElement) { - const displayText = message ? `${status}: ${message}` : status; - statusElement.textContent = displayText; - statusElement.style.color = isConnectedState ? "var(--accent-primary)" : "var(--error-700)"; +function updateConnectionStatus(code: string, label: string): void { + if (statusPill) { + statusPill.dataset.status = code; } + if (statusText) { + statusText.textContent = label; + } + + const isConnected = code === "connected"; + if (connectBtn) connectBtn.hidden = isConnected; + if (disconnectBtn) disconnectBtn.hidden = !isConnected; + parent.postMessage( { type: "update-connection-status", @@ -30,6 +42,34 @@ function updateConnectionStatus(code: string, status: string, isConnectedState: ); } +/** + * Updates the "Current task" display with the currently executing task name. + * + * @param taskName - the task name to display, or null to reset to "---" + */ +function updateCurrentTask(taskName: string | null): void { + if (currentTaskEl) { + currentTaskEl.textContent = taskName ?? "---"; + } + if (taskName === null) { + updateExecutedCode(null); + } +} + +/** + * Updates the executed code textarea with the last code run during task execution. + * + * @param code - the code string to display, or null to clear + */ +function updateExecutedCode(code: string | null): void { + if (executedCodeEl) { + executedCodeEl.value = code ?? ""; + } + if (copyCodeBtn) { + copyCodeBtn.disabled = !code; + } +} + /** * Sends a task response back to the MCP server via WebSocket. * @@ -49,7 +89,7 @@ function sendTaskResponse(response: any): void { */ function connectToMcpServer(baseUrl?: string, token?: string): void { if (ws?.readyState === WebSocket.OPEN) { - updateConnectionStatus("connected", "Already connected", true); + updateConnectionStatus("connected", "Connected"); return; } @@ -62,17 +102,22 @@ function connectToMcpServer(baseUrl?: string, token?: string): void { } ws = new WebSocket(wsUrl); - updateConnectionStatus("connecting", "Connecting...", false); + updateConnectionStatus("connecting", "Connecting..."); ws.onopen = () => { console.log("Connected to MCP server"); - updateConnectionStatus("connected", "Connected to MCP server", true); + updateConnectionStatus("connected", "Connected"); }; ws.onmessage = (event) => { try { console.log("Received from MCP server:", event.data); const request = JSON.parse(event.data); + // Track the current task received from the MCP server + if (request.task) { + updateCurrentTask(request.task); + updateExecutedCode(request.params?.code ?? null); + } // Forward the task request to the plugin for execution parent.postMessage(request, "*"); } catch (error) { @@ -84,8 +129,9 @@ function connectToMcpServer(baseUrl?: string, token?: string): void { // If we've send the error update we don't send the disconnect as well if (!wsError) { console.log("Disconnected from MCP server"); - const message = event.reason || undefined; - updateConnectionStatus("disconnected", "Disconnected", false, message); + const label = event.reason ? `Disconnected: ${event.reason}` : "Disconnected"; + updateConnectionStatus("disconnected", label); + updateCurrentTask(null); } ws = null; }; @@ -94,19 +140,34 @@ function connectToMcpServer(baseUrl?: string, token?: string): void { console.error("WebSocket error:", error); wsError = error; // note: WebSocket error events typically don't contain detailed error messages - updateConnectionStatus("error", "Connection error", false); + updateConnectionStatus("error", "Connection error"); }; } catch (error) { console.error("Failed to connect to MCP server:", error); - const message = error instanceof Error ? error.message : undefined; - updateConnectionStatus("error", "Connection failed", false, message); + const reason = error instanceof Error ? error.message : undefined; + const label = reason ? `Connection failed: ${reason}` : "Connection failed"; + updateConnectionStatus("error", label); } } -document.querySelector("[data-handler='connect-mcp']")?.addEventListener("click", () => { +copyCodeBtn?.addEventListener("click", () => { + const code = executedCodeEl?.value; + if (!code) return; + + navigator.clipboard.writeText(code).then(() => { + copyCodeBtn.classList.add("copied"); + setTimeout(() => copyCodeBtn.classList.remove("copied"), 1500); + }); +}); + +connectBtn?.addEventListener("click", () => { connectToMcpServer(); }); +disconnectBtn?.addEventListener("click", () => { + ws?.close(); +}); + // Listen plugin.ts messages window.addEventListener("message", (event) => { if (event.data.type === "start-server") { diff --git a/mcp/packages/plugin/src/plugin.ts b/mcp/packages/plugin/src/plugin.ts index e113f7adc3..e2b5bee38e 100644 --- a/mcp/packages/plugin/src/plugin.ts +++ b/mcp/packages/plugin/src/plugin.ts @@ -10,8 +10,8 @@ const taskHandlers: TaskHandler[] = [new ExecuteCodeTaskHandler()]; // Open the plugin UI (main.ts) penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}`, { - width: 158, - height: 200, + width: 236, + height: 210, hidden: !!mcp, } as any); diff --git a/mcp/packages/plugin/src/style.css b/mcp/packages/plugin/src/style.css index 030f2204e9..7061657b33 100644 --- a/mcp/packages/plugin/src/style.css +++ b/mcp/packages/plugin/src/style.css @@ -1,10 +1,178 @@ @import "@penpot/plugin-styles/styles.css"; body { - line-height: 1.5; - padding: 10px; + margin: 0; + padding: 0; } -p { - margin-block-end: 0.75rem; +.plugin-container { + display: flex; + flex-direction: column; + gap: var(--spacing-8); + padding: var(--spacing-16) var(--spacing-8); + box-sizing: border-box; +} + +/* ── Status pill ─────────────────────────────────────────────────── */ + +.status-pill { + display: flex; + align-items: center; + justify-content: center; + gap: var(--spacing-8); + padding: var(--spacing-8) var(--spacing-16); + border-radius: var(--spacing-8); + border: 1px solid var(--background-quaternary); + color: var(--foreground-secondary); + width: 100%; + box-sizing: border-box; +} + +.status-pill[data-status="connected"] { + border-color: var(--accent-primary); + color: var(--accent-primary); +} + +.status-pill[data-status="disconnected"], +.status-pill[data-status="error"] { + border-color: var(--error-500); + color: var(--error-500); +} + +.status-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: currentColor; + flex-shrink: 0; +} + +/* ── Collapsible section ─────────────────────────────────────────── */ + +.collapsible-section { + border: 1px solid var(--background-quaternary); + border-radius: var(--spacing-8); + overflow: hidden; +} + +.collapsible-header { + display: flex; + align-items: center; + gap: var(--spacing-8); + padding: var(--spacing-8) var(--spacing-12); + cursor: pointer; + color: var(--foreground-secondary); + list-style: none; + user-select: none; +} + +.collapsible-header::-webkit-details-marker { + display: none; +} + +.collapsible-arrow { + flex-shrink: 0; + transition: transform 0.2s ease; +} + +details[open] > .collapsible-header .collapsible-arrow { + transform: rotate(90deg); +} + +.collapsible-body { + display: flex; + flex-direction: column; + gap: var(--spacing-4); + padding: var(--spacing-4) var(--spacing-12) var(--spacing-12); + border-top: 1px solid var(--background-quaternary); +} + +/* ── Tool section ────────────────────────────────────────────────── */ + +.tool-label { + color: var(--foreground-secondary); +} + +.tool-display { + display: flex; + align-items: center; + gap: var(--spacing-8); + padding: var(--spacing-8) var(--spacing-12); + border-radius: var(--spacing-8); + background-color: var(--background-tertiary); + color: var(--foreground-secondary); + min-height: 32px; + box-sizing: border-box; +} + +.tool-icon { + flex-shrink: 0; + opacity: 0.7; +} + +/* ── Code section ────────────────────────────────────────────────── */ + +.code-section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: var(--spacing-8); +} + +.code-textarea { + width: 100%; + height: 100px; + resize: vertical; + padding: var(--spacing-8) var(--spacing-12); + border-radius: var(--spacing-8); + border: 1px solid var(--background-quaternary); + background-color: var(--background-tertiary); + color: var(--foreground-secondary); + font-family: monospace; + font-size: 11px; + line-height: 1.5; + box-sizing: border-box; + outline: none; +} + +.copy-btn { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + border: 1px solid var(--background-quaternary); + border-radius: var(--spacing-4); + background-color: transparent; + color: var(--foreground-secondary); + cursor: pointer; + flex-shrink: 0; + transition: + background-color 0.15s ease, + color 0.15s ease; +} + +.copy-btn:hover:not(:disabled) { + background-color: var(--background-tertiary); + color: var(--foreground-primary); +} + +.copy-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.copy-btn.copied { + color: var(--accent-primary); + border-color: var(--accent-primary); +} + +/* ── Action buttons ──────────────────────────────────────────────── */ + +#connect-btn, +#disconnect-btn { + width: 100%; + margin-top: var(--spacing-4); }