mirror of
https://github.com/penpot/penpot.git
synced 2026-04-29 05:08:08 +00:00
If the MCP version (as given in mcp/package.json) does not match the Penpot version (as given by penpot.version), display a warning message in the plugin UI. This is important for users running the local MCP server, as it is a common failure mode to combine the MCP server with an incompatible Penpot version.
202 lines
6.9 KiB
TypeScript
202 lines
6.9 KiB
TypeScript
import "./style.css";
|
|
|
|
// get the current theme from the URL
|
|
const searchParams = new URLSearchParams(window.location.hash.split("?")[1]);
|
|
document.body.dataset.theme = searchParams.get("theme") ?? "light";
|
|
|
|
// WebSocket connection management
|
|
let ws: WebSocket | null = null;
|
|
|
|
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;
|
|
const versionWarningEl = document.getElementById("version-warning") as HTMLElement;
|
|
const versionWarningTextEl = document.getElementById("version-warning-text") as HTMLElement;
|
|
|
|
/**
|
|
* Updates the status pill and button visibility based on connection state.
|
|
*
|
|
* @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, 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",
|
|
status: code,
|
|
},
|
|
"*"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param response - The response containing task ID and result
|
|
*/
|
|
function sendTaskResponse(response: any): void {
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(JSON.stringify(response));
|
|
console.log("Sent response to MCP server:", response);
|
|
} else {
|
|
console.error("WebSocket not connected, cannot send response");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Establishes a WebSocket connection to the MCP server.
|
|
*/
|
|
function connectToMcpServer(baseUrl?: string, token?: string): void {
|
|
if (ws?.readyState === WebSocket.OPEN) {
|
|
updateConnectionStatus("connected", "Connected");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let wsUrl = baseUrl || PENPOT_MCP_WEBSOCKET_URL;
|
|
let wsError: unknown | undefined;
|
|
|
|
if (token) {
|
|
wsUrl += `?userToken=${encodeURIComponent(token)}`;
|
|
}
|
|
|
|
ws = new WebSocket(wsUrl);
|
|
updateConnectionStatus("connecting", "Connecting...");
|
|
|
|
ws.onopen = () => {
|
|
setTimeout(() => {
|
|
if (ws) {
|
|
console.log("Connected to MCP server");
|
|
updateConnectionStatus("connected", "Connected");
|
|
}
|
|
}, 100);
|
|
};
|
|
|
|
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) {
|
|
console.error("Failed to parse WebSocket message:", error);
|
|
}
|
|
};
|
|
|
|
ws.onclose = (event: CloseEvent) => {
|
|
// If we've send the error update we don't send the disconnect as well
|
|
if (!wsError) {
|
|
console.log("Disconnected from MCP server");
|
|
const label = event.reason ? `Disconnected: ${event.reason}` : "Disconnected";
|
|
updateConnectionStatus("disconnected", label);
|
|
updateCurrentTask(null);
|
|
}
|
|
ws = null;
|
|
};
|
|
|
|
ws.onerror = (error) => {
|
|
console.error("WebSocket error:", error);
|
|
wsError = error;
|
|
// note: WebSocket error events typically don't contain detailed error messages
|
|
updateConnectionStatus("error", "Connection error");
|
|
};
|
|
} catch (error) {
|
|
console.error("Failed to connect to MCP server:", error);
|
|
const reason = error instanceof Error ? error.message : undefined;
|
|
const label = reason ? `Connection failed: ${reason}` : "Connection failed";
|
|
updateConnectionStatus("error", label);
|
|
}
|
|
}
|
|
|
|
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") {
|
|
connectToMcpServer(event.data.url, event.data.token);
|
|
}
|
|
if (event.data.type === "version-mismatch") {
|
|
if (versionWarningEl && versionWarningTextEl) {
|
|
versionWarningTextEl.innerHTML =
|
|
`<b>Version mismatch detected</b>: This version of the MCP server is intended for Penpot ` +
|
|
`${event.data.mcpVersion} while the current version is ${event.data.penpotVersion}. ` +
|
|
`Executions may not work or produce suboptimal results.`;
|
|
versionWarningEl.hidden = false;
|
|
}
|
|
}
|
|
if (event.data.type === "stop-server") {
|
|
ws?.close();
|
|
} else if (event.data.source === "penpot") {
|
|
document.body.dataset.theme = event.data.theme;
|
|
} else if (event.data.type === "task-response") {
|
|
// Forward task response back to MCP server
|
|
sendTaskResponse(event.data.response);
|
|
}
|
|
});
|
|
|
|
parent.postMessage({ type: "ui-initialized" }, "*");
|