@biolab/talk-to-figma 0.3.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -199
- package/dist/cli.cjs +2780 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +2760 -0
- package/dist/cli.js.map +1 -0
- package/dist/relay.cjs +159 -0
- package/dist/relay.cjs.map +1 -0
- package/dist/relay.d.cts +4 -0
- package/dist/relay.d.ts +4 -0
- package/dist/relay.js +136 -0
- package/dist/relay.js.map +1 -0
- package/dist/talk_to_figma_mcp/server.cjs.map +1 -0
- package/dist/talk_to_figma_mcp/server.d.cts +1 -0
- package/dist/talk_to_figma_mcp/server.d.ts +1 -0
- package/dist/talk_to_figma_mcp/server.js.map +1 -0
- package/figma-plugin/code.js +4340 -0
- package/figma-plugin/figma-plugin.zip +0 -0
- package/figma-plugin/manifest.json +23 -0
- package/figma-plugin/setcharacters.js +215 -0
- package/figma-plugin/ui.html +834 -0
- package/package.json +5 -3
- package/dist/server.cjs.map +0 -1
- package/dist/server.js.map +0 -1
- /package/dist/{server.d.cts → cli.d.cts} +0 -0
- /package/dist/{server.d.ts → cli.d.ts} +0 -0
- /package/dist/{server.cjs → talk_to_figma_mcp/server.cjs} +0 -0
- /package/dist/{server.js → talk_to_figma_mcp/server.js} +0 -0
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Talk to Figma MCP</title>
|
|
6
|
+
<style>
|
|
7
|
+
body {
|
|
8
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
9
|
+
Helvetica, Arial, sans-serif;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 20px;
|
|
12
|
+
color: #e0e0e0;
|
|
13
|
+
background-color: #1e1e1e;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.container {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
height: 100%;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
h1 {
|
|
23
|
+
font-size: 16px;
|
|
24
|
+
font-weight: 600;
|
|
25
|
+
margin-bottom: 10px;
|
|
26
|
+
color: #ffffff;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
h2 {
|
|
30
|
+
font-size: 14px;
|
|
31
|
+
font-weight: 600;
|
|
32
|
+
margin-top: 20px;
|
|
33
|
+
margin-bottom: 8px;
|
|
34
|
+
color: #ffffff;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
button {
|
|
38
|
+
background-color: #18a0fb;
|
|
39
|
+
border: none;
|
|
40
|
+
color: white;
|
|
41
|
+
padding: 8px 12px;
|
|
42
|
+
border-radius: 6px;
|
|
43
|
+
margin-top: 8px;
|
|
44
|
+
margin-bottom: 8px;
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
font-size: 14px;
|
|
47
|
+
transition: background-color 0.2s;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
button:hover {
|
|
51
|
+
background-color: #0d8ee0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
button.secondary {
|
|
55
|
+
background-color: #3d3d3d;
|
|
56
|
+
color: #e0e0e0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
button.secondary:hover {
|
|
60
|
+
background-color: #4d4d4d;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
button:disabled {
|
|
64
|
+
background-color: #333333;
|
|
65
|
+
color: #666666;
|
|
66
|
+
cursor: not-allowed;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
input {
|
|
70
|
+
border: 1px solid #444444;
|
|
71
|
+
border-radius: 4px;
|
|
72
|
+
padding: 8px;
|
|
73
|
+
margin-bottom: 12px;
|
|
74
|
+
font-size: 14px;
|
|
75
|
+
width: 100%;
|
|
76
|
+
box-sizing: border-box;
|
|
77
|
+
background-color: #2d2d2d;
|
|
78
|
+
color: #e0e0e0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
label {
|
|
82
|
+
display: block;
|
|
83
|
+
margin-bottom: 4px;
|
|
84
|
+
font-size: 12px;
|
|
85
|
+
font-weight: 500;
|
|
86
|
+
color: #cccccc;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.status {
|
|
90
|
+
margin-top: 16px;
|
|
91
|
+
padding: 12px;
|
|
92
|
+
border-radius: 6px;
|
|
93
|
+
font-size: 14px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.status.connected {
|
|
97
|
+
background-color: #1a472a;
|
|
98
|
+
color: #4ade80;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.status.disconnected {
|
|
102
|
+
background-color: #471a1a;
|
|
103
|
+
color: #ff9999;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.status.info {
|
|
107
|
+
background-color: #1a3147;
|
|
108
|
+
color: #66b3ff;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.toggle-container {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
margin-bottom: 12px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.toggle-switch {
|
|
118
|
+
position: relative;
|
|
119
|
+
display: inline-block;
|
|
120
|
+
width: 40px;
|
|
121
|
+
height: 20px;
|
|
122
|
+
margin-right: 8px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.toggle-switch input {
|
|
126
|
+
opacity: 0;
|
|
127
|
+
width: 0;
|
|
128
|
+
height: 0;
|
|
129
|
+
margin: 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.toggle-slider {
|
|
133
|
+
position: absolute;
|
|
134
|
+
cursor: pointer;
|
|
135
|
+
top: 0;
|
|
136
|
+
left: 0;
|
|
137
|
+
right: 0;
|
|
138
|
+
bottom: 0;
|
|
139
|
+
background-color: #444444;
|
|
140
|
+
transition: 0.4s;
|
|
141
|
+
border-radius: 20px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.toggle-slider:before {
|
|
145
|
+
position: absolute;
|
|
146
|
+
content: "";
|
|
147
|
+
height: 16px;
|
|
148
|
+
width: 16px;
|
|
149
|
+
left: 2px;
|
|
150
|
+
bottom: 2px;
|
|
151
|
+
background-color: white;
|
|
152
|
+
transition: 0.4s;
|
|
153
|
+
border-radius: 50%;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
input:checked + .toggle-slider {
|
|
157
|
+
background-color: #18a0fb;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
input:checked + .toggle-slider:before {
|
|
161
|
+
transform: translateX(20px);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.section {
|
|
165
|
+
margin-bottom: 24px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.hidden {
|
|
169
|
+
display: none;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.logo {
|
|
173
|
+
width: 50px;
|
|
174
|
+
height: 50px;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.header {
|
|
178
|
+
display: flex;
|
|
179
|
+
align-items: center;
|
|
180
|
+
margin-bottom: 16px;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.header-text {
|
|
184
|
+
margin-left: 12px;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.header-text h1 {
|
|
188
|
+
margin: 0;
|
|
189
|
+
font-size: 16px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.header-text p {
|
|
193
|
+
margin: 4px 0 0 0;
|
|
194
|
+
font-size: 12px;
|
|
195
|
+
color: #999999;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.tabs {
|
|
199
|
+
display: flex;
|
|
200
|
+
border-bottom: 1px solid #444444;
|
|
201
|
+
margin-bottom: 16px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.tab {
|
|
205
|
+
padding: 8px 16px;
|
|
206
|
+
cursor: pointer;
|
|
207
|
+
font-size: 14px;
|
|
208
|
+
font-weight: 500;
|
|
209
|
+
color: #999999;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.tab.active {
|
|
213
|
+
border-bottom: 2px solid #18a0fb;
|
|
214
|
+
color: #18a0fb;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.tab-content {
|
|
218
|
+
display: none;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.tab-content.active {
|
|
222
|
+
display: block;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.link {
|
|
226
|
+
color: #18a0fb;
|
|
227
|
+
text-decoration: none;
|
|
228
|
+
cursor: pointer;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.link:hover {
|
|
232
|
+
text-decoration: underline;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.header-logo {
|
|
236
|
+
padding: 16px;
|
|
237
|
+
border-radius: 16px;
|
|
238
|
+
background-color: #333;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.header-logo-image {
|
|
242
|
+
width: 24px;
|
|
243
|
+
height: 24px;
|
|
244
|
+
object-fit: contain;
|
|
245
|
+
}
|
|
246
|
+
/* Progress styles */
|
|
247
|
+
.operation-complete {
|
|
248
|
+
color: #4ade80;
|
|
249
|
+
}
|
|
250
|
+
.operation-error {
|
|
251
|
+
color: #ff9999;
|
|
252
|
+
}
|
|
253
|
+
</style>
|
|
254
|
+
</head>
|
|
255
|
+
|
|
256
|
+
<body>
|
|
257
|
+
<div class="container">
|
|
258
|
+
<div class="header">
|
|
259
|
+
<div class="header-text">
|
|
260
|
+
<h1>Talk To Figma MCP Plugin</h1>
|
|
261
|
+
<p>Connect Figma to AI agents using MCP</p>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div class="tabs">
|
|
266
|
+
<div id="tab-connection" class="tab active">Connection</div>
|
|
267
|
+
<div id="tab-about" class="tab">About</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div id="content-connection" class="tab-content active">
|
|
271
|
+
<div class="section">
|
|
272
|
+
<div style="margin-bottom: 16px">
|
|
273
|
+
<button id="btn-connect" class="primary" style="width: 100%">
|
|
274
|
+
Connect
|
|
275
|
+
</button>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<div id="port-container">
|
|
279
|
+
<label for="port">WebSocket Server Port</label>
|
|
280
|
+
<input
|
|
281
|
+
type="number"
|
|
282
|
+
id="port"
|
|
283
|
+
placeholder="3055"
|
|
284
|
+
value="3055"
|
|
285
|
+
min="1024"
|
|
286
|
+
max="65535"
|
|
287
|
+
/>
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
<div id="connection-status" class="status disconnected">
|
|
292
|
+
Not connected to MCP server
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<div id="mcp-config" class="section hidden">
|
|
296
|
+
<h2>MCP Configuration</h2>
|
|
297
|
+
<p>
|
|
298
|
+
Copy this configuration to your <code>mcp.json</code> file in
|
|
299
|
+
Cursor:
|
|
300
|
+
</p>
|
|
301
|
+
<textarea
|
|
302
|
+
id="mcp-json"
|
|
303
|
+
rows="5"
|
|
304
|
+
readonly
|
|
305
|
+
style="
|
|
306
|
+
width: 100%;
|
|
307
|
+
background: #2d2d2d;
|
|
308
|
+
color: #e0e0e0;
|
|
309
|
+
padding: 12px;
|
|
310
|
+
border: 1px solid #444444;
|
|
311
|
+
border-radius: 4px;
|
|
312
|
+
font-family: monospace;
|
|
313
|
+
margin-bottom: 8px;
|
|
314
|
+
"
|
|
315
|
+
></textarea>
|
|
316
|
+
<button id="btn-copy" class="secondary">Copy to Clipboard</button>
|
|
317
|
+
</div>
|
|
318
|
+
|
|
319
|
+
<!-- Add Progress Bar Section -->
|
|
320
|
+
<div id="progress-container" class="section hidden">
|
|
321
|
+
<h2>Operation Progress</h2>
|
|
322
|
+
<div id="progress-message">No operation in progress</div>
|
|
323
|
+
<div
|
|
324
|
+
style="
|
|
325
|
+
width: 100%;
|
|
326
|
+
background-color: #444;
|
|
327
|
+
border-radius: 4px;
|
|
328
|
+
margin-top: 8px;
|
|
329
|
+
"
|
|
330
|
+
>
|
|
331
|
+
<div
|
|
332
|
+
id="progress-bar"
|
|
333
|
+
style="
|
|
334
|
+
width: 0%;
|
|
335
|
+
height: 8px;
|
|
336
|
+
background-color: #18a0fb;
|
|
337
|
+
border-radius: 4px;
|
|
338
|
+
transition: width 0.3s;
|
|
339
|
+
"
|
|
340
|
+
></div>
|
|
341
|
+
</div>
|
|
342
|
+
<div
|
|
343
|
+
style="
|
|
344
|
+
display: flex;
|
|
345
|
+
justify-content: space-between;
|
|
346
|
+
margin-top: 4px;
|
|
347
|
+
font-size: 12px;
|
|
348
|
+
"
|
|
349
|
+
>
|
|
350
|
+
<div id="progress-status">Not started</div>
|
|
351
|
+
<div id="progress-percentage">0%</div>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<div id="content-about" class="tab-content">
|
|
357
|
+
<div class="section">
|
|
358
|
+
<h2>About Talk To Figma Plugin</h2>
|
|
359
|
+
<p>
|
|
360
|
+
This plugin lets AI agents (Cursor, Claude Code) read and modify
|
|
361
|
+
Figma designs via MCP.
|
|
362
|
+
<a
|
|
363
|
+
class="link"
|
|
364
|
+
onclick="window.open(`https://github.com/anthropics/cursor-talk-to-figma-mcp`, '_blank')"
|
|
365
|
+
>Github</a
|
|
366
|
+
>
|
|
367
|
+
</p>
|
|
368
|
+
<p>Version: 0.4.0</p>
|
|
369
|
+
|
|
370
|
+
<h2>How to Use</h2>
|
|
371
|
+
<ol>
|
|
372
|
+
<li>Start the relay: <code>bunx @biolab/talk-to-figma --relay</code></li>
|
|
373
|
+
<li>Connect to the relay using the port number (default: 3055)</li>
|
|
374
|
+
<li>In your AI agent, call <code>join_channel</code> with the channel name shown above</li>
|
|
375
|
+
</ol>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<script>
|
|
381
|
+
// WebSocket connection state
|
|
382
|
+
const state = {
|
|
383
|
+
connected: false,
|
|
384
|
+
socket: null,
|
|
385
|
+
serverPort: 3055,
|
|
386
|
+
pendingRequests: new Map(),
|
|
387
|
+
channel: null,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// UI Elements
|
|
391
|
+
const portInput = document.getElementById("port");
|
|
392
|
+
const connectButton = document.getElementById("btn-connect");
|
|
393
|
+
const connectionStatus = document.getElementById("connection-status");
|
|
394
|
+
|
|
395
|
+
// Tabs
|
|
396
|
+
const tabs = document.querySelectorAll(".tab");
|
|
397
|
+
const tabContents = document.querySelectorAll(".tab-content");
|
|
398
|
+
|
|
399
|
+
// Add UI elements for progress tracking
|
|
400
|
+
const progressContainer = document.getElementById("progress-container");
|
|
401
|
+
const progressBar = document.getElementById("progress-bar");
|
|
402
|
+
const progressMessage = document.getElementById("progress-message");
|
|
403
|
+
const progressStatus = document.getElementById("progress-status");
|
|
404
|
+
const progressPercentage = document.getElementById("progress-percentage");
|
|
405
|
+
|
|
406
|
+
// Initialize UI
|
|
407
|
+
function updateConnectionStatus(isConnected, message) {
|
|
408
|
+
state.connected = isConnected;
|
|
409
|
+
|
|
410
|
+
let statusMessage =
|
|
411
|
+
message ||
|
|
412
|
+
(isConnected
|
|
413
|
+
? "Connected to MCP server"
|
|
414
|
+
: "Not connected to MCP server");
|
|
415
|
+
|
|
416
|
+
// Add instructions for localhost when disconnected
|
|
417
|
+
if (!isConnected) {
|
|
418
|
+
statusMessage += `<br><br>Run this in your terminal, then connect<br><code>bunx @biolab/talk-to-figma --relay</code>`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
connectionStatus.innerHTML = statusMessage;
|
|
422
|
+
connectionStatus.className = `status ${
|
|
423
|
+
isConnected ? "connected" : "disconnected"
|
|
424
|
+
}`;
|
|
425
|
+
|
|
426
|
+
// Update connect button text and style based on connection state
|
|
427
|
+
connectButton.textContent = isConnected ? "Disconnect" : "Connect";
|
|
428
|
+
connectButton.className = isConnected ? "secondary" : "primary";
|
|
429
|
+
portInput.disabled = isConnected;
|
|
430
|
+
|
|
431
|
+
// Show/hide MCP config section
|
|
432
|
+
const mcpConfig = document.getElementById("mcp-config");
|
|
433
|
+
mcpConfig.className = `section ${isConnected ? "" : "hidden"}`;
|
|
434
|
+
|
|
435
|
+
// Update MCP config text when showing
|
|
436
|
+
if (isConnected) {
|
|
437
|
+
updateMcpConfig();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Connect to WebSocket server
|
|
442
|
+
async function connectToServer(port) {
|
|
443
|
+
try {
|
|
444
|
+
if (state.connected && state.socket) {
|
|
445
|
+
updateConnectionStatus(true, "Already connected to server");
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
state.serverPort = port;
|
|
450
|
+
const wsUrl = `ws://localhost:${port}`;
|
|
451
|
+
|
|
452
|
+
state.socket = new WebSocket(wsUrl);
|
|
453
|
+
|
|
454
|
+
state.socket.onopen = () => {
|
|
455
|
+
// Generate random channel name
|
|
456
|
+
const channelName = generateChannelName();
|
|
457
|
+
console.log("Joining channel:", channelName);
|
|
458
|
+
state.channel = channelName;
|
|
459
|
+
|
|
460
|
+
// Join the channel using the same format as App.tsx
|
|
461
|
+
state.socket.send(
|
|
462
|
+
JSON.stringify({
|
|
463
|
+
type: "join",
|
|
464
|
+
channel: channelName.trim(),
|
|
465
|
+
})
|
|
466
|
+
);
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
state.socket.onmessage = (event) => {
|
|
470
|
+
try {
|
|
471
|
+
const data = JSON.parse(event.data);
|
|
472
|
+
console.log("Received message:", data);
|
|
473
|
+
|
|
474
|
+
if (data.type === "system") {
|
|
475
|
+
// Successfully joined channel
|
|
476
|
+
if (data.message && data.message.result) {
|
|
477
|
+
state.connected = true;
|
|
478
|
+
const channelName = data.channel;
|
|
479
|
+
updateConnectionStatus(
|
|
480
|
+
true,
|
|
481
|
+
`Connected to server in channel: <strong>${channelName}</strong>`
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// Notify the plugin code
|
|
485
|
+
parent.postMessage(
|
|
486
|
+
{
|
|
487
|
+
pluginMessage: {
|
|
488
|
+
type: "notify",
|
|
489
|
+
message: `Connected to MCP server on port ${port} in channel: ${channelName}`,
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
"*"
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
} else if (data.type === "error") {
|
|
496
|
+
console.error("Error:", data.message);
|
|
497
|
+
updateConnectionStatus(false, `Error: ${data.message}`);
|
|
498
|
+
state.socket.close();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
handleSocketMessage(data);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
console.error("Error parsing message:", error);
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
state.socket.onclose = () => {
|
|
508
|
+
state.connected = false;
|
|
509
|
+
state.socket = null;
|
|
510
|
+
updateConnectionStatus(false, "Disconnected from server");
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
state.socket.onerror = (error) => {
|
|
514
|
+
console.error("WebSocket error:", error);
|
|
515
|
+
updateConnectionStatus(false, "Connection error");
|
|
516
|
+
state.connected = false;
|
|
517
|
+
state.socket = null;
|
|
518
|
+
};
|
|
519
|
+
} catch (error) {
|
|
520
|
+
console.error("Connection error:", error);
|
|
521
|
+
updateConnectionStatus(
|
|
522
|
+
false,
|
|
523
|
+
`Connection error: ${error.message || "Unknown error"}`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Disconnect from websocket server
|
|
529
|
+
function disconnectFromServer() {
|
|
530
|
+
if (state.socket) {
|
|
531
|
+
state.socket.close();
|
|
532
|
+
state.socket = null;
|
|
533
|
+
state.connected = false;
|
|
534
|
+
updateConnectionStatus(false, "Disconnected from server");
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Handle messages from the WebSocket
|
|
539
|
+
async function handleSocketMessage(payload) {
|
|
540
|
+
const data = payload.message;
|
|
541
|
+
console.log("handleSocketMessage", data);
|
|
542
|
+
|
|
543
|
+
// If it's a response to a previous request
|
|
544
|
+
if (data.id && state.pendingRequests.has(data.id)) {
|
|
545
|
+
const { resolve, reject } = state.pendingRequests.get(data.id);
|
|
546
|
+
state.pendingRequests.delete(data.id);
|
|
547
|
+
|
|
548
|
+
if (data.error) {
|
|
549
|
+
reject(new Error(data.error));
|
|
550
|
+
} else {
|
|
551
|
+
resolve(data.result);
|
|
552
|
+
}
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// If it's a new command
|
|
557
|
+
if (data.command) {
|
|
558
|
+
try {
|
|
559
|
+
// Send the command to the plugin code
|
|
560
|
+
parent.postMessage(
|
|
561
|
+
{
|
|
562
|
+
pluginMessage: {
|
|
563
|
+
type: "execute-command",
|
|
564
|
+
id: data.id,
|
|
565
|
+
command: data.command,
|
|
566
|
+
params: data.params,
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
"*"
|
|
570
|
+
);
|
|
571
|
+
} catch (error) {
|
|
572
|
+
// Send error back to WebSocket
|
|
573
|
+
sendErrorResponse(
|
|
574
|
+
data.id,
|
|
575
|
+
error.message || "Error executing command"
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Send a command to the WebSocket server
|
|
582
|
+
async function sendCommand(command, params) {
|
|
583
|
+
return new Promise((resolve, reject) => {
|
|
584
|
+
if (!state.connected || !state.socket) {
|
|
585
|
+
reject(new Error("Not connected to server"));
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const id = generateId();
|
|
590
|
+
state.pendingRequests.set(id, { resolve, reject });
|
|
591
|
+
|
|
592
|
+
state.socket.send(
|
|
593
|
+
JSON.stringify({
|
|
594
|
+
id,
|
|
595
|
+
type: "message",
|
|
596
|
+
channel: state.channel,
|
|
597
|
+
message: {
|
|
598
|
+
id,
|
|
599
|
+
command,
|
|
600
|
+
params,
|
|
601
|
+
},
|
|
602
|
+
})
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
// Set timeout to reject the promise after 30 seconds
|
|
606
|
+
setTimeout(() => {
|
|
607
|
+
if (state.pendingRequests.has(id)) {
|
|
608
|
+
state.pendingRequests.delete(id);
|
|
609
|
+
reject(new Error("Request timed out"));
|
|
610
|
+
}
|
|
611
|
+
}, 30000);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Send success response back to WebSocket
|
|
616
|
+
function sendSuccessResponse(id, result) {
|
|
617
|
+
if (!state.connected || !state.socket) {
|
|
618
|
+
console.error("Cannot send response: socket not connected");
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
state.socket.send(
|
|
623
|
+
JSON.stringify({
|
|
624
|
+
id,
|
|
625
|
+
type: "message",
|
|
626
|
+
channel: state.channel,
|
|
627
|
+
message: {
|
|
628
|
+
id,
|
|
629
|
+
result,
|
|
630
|
+
},
|
|
631
|
+
})
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Send error response back to WebSocket
|
|
636
|
+
function sendErrorResponse(id, errorMessage) {
|
|
637
|
+
if (!state.connected || !state.socket) {
|
|
638
|
+
console.error("Cannot send error response: socket not connected");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
state.socket.send(
|
|
643
|
+
JSON.stringify({
|
|
644
|
+
id,
|
|
645
|
+
type: "message",
|
|
646
|
+
channel: state.channel,
|
|
647
|
+
message: {
|
|
648
|
+
id,
|
|
649
|
+
error: errorMessage,
|
|
650
|
+
result: {},
|
|
651
|
+
},
|
|
652
|
+
})
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Helper to generate unique IDs
|
|
657
|
+
function generateId() {
|
|
658
|
+
return (
|
|
659
|
+
Date.now().toString(36) + Math.random().toString(36).substr(2, 5)
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Add this function after the generateId() function
|
|
664
|
+
function generateChannelName() {
|
|
665
|
+
const characters = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
666
|
+
let result = "";
|
|
667
|
+
for (let i = 0; i < 8; i++) {
|
|
668
|
+
result += characters.charAt(
|
|
669
|
+
Math.floor(Math.random() * characters.length)
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
return result;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Tab switching
|
|
676
|
+
tabs.forEach((tab) => {
|
|
677
|
+
tab.addEventListener("click", () => {
|
|
678
|
+
tabs.forEach((t) => t.classList.remove("active"));
|
|
679
|
+
tabContents.forEach((c) => c.classList.remove("active"));
|
|
680
|
+
|
|
681
|
+
tab.classList.add("active");
|
|
682
|
+
const contentId = "content-" + tab.id.split("-")[1];
|
|
683
|
+
document.getElementById(contentId).classList.add("active");
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// Replace the separate connect/disconnect button listeners with a single toggle
|
|
688
|
+
connectButton.addEventListener("click", () => {
|
|
689
|
+
if (state.connected) {
|
|
690
|
+
updateConnectionStatus(false, "Disconnecting...");
|
|
691
|
+
connectionStatus.className = "status info";
|
|
692
|
+
disconnectFromServer();
|
|
693
|
+
} else {
|
|
694
|
+
const port = parseInt(portInput.value, 10) || 3055;
|
|
695
|
+
updateConnectionStatus(false, "Connecting...");
|
|
696
|
+
connectionStatus.className = "status info";
|
|
697
|
+
connectToServer(port);
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// Function to update progress UI
|
|
702
|
+
function updateProgressUI(progressData) {
|
|
703
|
+
// Show progress container if hidden
|
|
704
|
+
progressContainer.classList.remove("hidden");
|
|
705
|
+
|
|
706
|
+
// Update progress bar
|
|
707
|
+
const progress = progressData.progress || 0;
|
|
708
|
+
progressBar.style.width = `${progress}%`;
|
|
709
|
+
progressPercentage.textContent = `${progress}%`;
|
|
710
|
+
|
|
711
|
+
// Update message
|
|
712
|
+
progressMessage.textContent =
|
|
713
|
+
progressData.message || "Operation in progress";
|
|
714
|
+
|
|
715
|
+
// Update status text based on operation state
|
|
716
|
+
if (progressData.status === "started") {
|
|
717
|
+
progressStatus.textContent = "Started";
|
|
718
|
+
progressStatus.className = "";
|
|
719
|
+
} else if (progressData.status === "in_progress") {
|
|
720
|
+
progressStatus.textContent = "In Progress";
|
|
721
|
+
progressStatus.className = "";
|
|
722
|
+
} else if (progressData.status === "completed") {
|
|
723
|
+
progressStatus.textContent = "Completed";
|
|
724
|
+
progressStatus.className = "operation-complete";
|
|
725
|
+
|
|
726
|
+
// Hide progress container after 5 seconds
|
|
727
|
+
setTimeout(() => {
|
|
728
|
+
progressContainer.classList.add("hidden");
|
|
729
|
+
}, 5000);
|
|
730
|
+
} else if (progressData.status === "error") {
|
|
731
|
+
progressStatus.textContent = "Error";
|
|
732
|
+
progressStatus.className = "operation-error";
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Send operation progress update to server
|
|
737
|
+
function sendProgressUpdateToServer(progressData) {
|
|
738
|
+
if (!state.connected || !state.socket) {
|
|
739
|
+
console.error("Cannot send progress update: socket not connected");
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
console.log("Sending progress update to server:", progressData);
|
|
744
|
+
|
|
745
|
+
state.socket.send(
|
|
746
|
+
JSON.stringify({
|
|
747
|
+
id: progressData.commandId,
|
|
748
|
+
type: "progress_update",
|
|
749
|
+
channel: state.channel,
|
|
750
|
+
message: {
|
|
751
|
+
id: progressData.commandId,
|
|
752
|
+
type: "progress_update",
|
|
753
|
+
data: progressData,
|
|
754
|
+
},
|
|
755
|
+
})
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Reset progress UI
|
|
760
|
+
function resetProgressUI() {
|
|
761
|
+
progressContainer.classList.add("hidden");
|
|
762
|
+
progressBar.style.width = "0%";
|
|
763
|
+
progressMessage.textContent = "No operation in progress";
|
|
764
|
+
progressStatus.textContent = "Not started";
|
|
765
|
+
progressStatus.className = "";
|
|
766
|
+
progressPercentage.textContent = "0%";
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Listen for messages from the plugin code
|
|
770
|
+
window.onmessage = (event) => {
|
|
771
|
+
const message = event.data.pluginMessage;
|
|
772
|
+
if (!message) return;
|
|
773
|
+
|
|
774
|
+
console.log("Received message from plugin:", message);
|
|
775
|
+
|
|
776
|
+
switch (message.type) {
|
|
777
|
+
case "connection-status":
|
|
778
|
+
updateConnectionStatus(message.connected, message.message);
|
|
779
|
+
break;
|
|
780
|
+
case "auto-connect":
|
|
781
|
+
connectButton.click();
|
|
782
|
+
break;
|
|
783
|
+
case "auto-disconnect":
|
|
784
|
+
connectButton.click();
|
|
785
|
+
break;
|
|
786
|
+
case "command-result":
|
|
787
|
+
// Forward the result from plugin code back to WebSocket
|
|
788
|
+
sendSuccessResponse(message.id, message.result);
|
|
789
|
+
break;
|
|
790
|
+
case "command-error":
|
|
791
|
+
// Forward the error from plugin code back to WebSocket
|
|
792
|
+
sendErrorResponse(message.id, message.error);
|
|
793
|
+
break;
|
|
794
|
+
case "command_progress":
|
|
795
|
+
// Update UI with progress information
|
|
796
|
+
updateProgressUI(message);
|
|
797
|
+
// Forward progress update to server
|
|
798
|
+
sendProgressUpdateToServer(message);
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
// Add this to the script section, after all the other event listeners
|
|
804
|
+
document.getElementById("btn-copy").addEventListener("click", () => {
|
|
805
|
+
const textarea = document.getElementById("mcp-json");
|
|
806
|
+
textarea.select();
|
|
807
|
+
document.execCommand("copy");
|
|
808
|
+
const button = document.getElementById("btn-copy");
|
|
809
|
+
button.textContent = "Copied!";
|
|
810
|
+
setTimeout(() => {
|
|
811
|
+
button.textContent = "Copy to Clipboard";
|
|
812
|
+
}, 2000);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
// Add this function to the script section
|
|
816
|
+
function updateMcpConfig() {
|
|
817
|
+
const mcpConfig = {
|
|
818
|
+
mcpServers: {
|
|
819
|
+
TalkToFigma: {
|
|
820
|
+
command: "bunx",
|
|
821
|
+
args: ["@biolab/talk-to-figma@latest"],
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
document.getElementById("mcp-json").value = JSON.stringify(
|
|
827
|
+
mcpConfig,
|
|
828
|
+
null,
|
|
829
|
+
2
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
</script>
|
|
833
|
+
</body>
|
|
834
|
+
</html>
|