figma-rust-mcp 2.8.2
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/LICENSE +21 -0
- package/README.md +189 -0
- package/bin/figma-mcp.js +445 -0
- package/package.json +60 -0
- package/plugin/code.js +5257 -0
- package/plugin/icon16.png +0 -0
- package/plugin/icon32.png +0 -0
- package/plugin/manifest.json +24 -0
- package/plugin/ui.html +998 -0
- package/scripts/build-plugin.js +34 -0
- package/scripts/test-rust-mcp.sh +40 -0
package/plugin/ui.html
ADDED
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Figma MCP</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* ── DESIGN TOKENS ── */
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #09090f;
|
|
11
|
+
--surface-1: #0f1117;
|
|
12
|
+
--surface-2: #141720;
|
|
13
|
+
--surface-3: #1a1f2e;
|
|
14
|
+
--surface-4: #222844;
|
|
15
|
+
--border-1: rgba(255,255,255,0.05);
|
|
16
|
+
--border-2: rgba(255,255,255,0.10);
|
|
17
|
+
--border-3: rgba(255,255,255,0.16);
|
|
18
|
+
--text-1: #f0f2f8;
|
|
19
|
+
--text-2: #8b95b0;
|
|
20
|
+
--text-3: #4e5873;
|
|
21
|
+
--accent: #6366f1;
|
|
22
|
+
--accent-2: #06b6d4;
|
|
23
|
+
--green: #22d3a0;
|
|
24
|
+
--amber: #f59e0b;
|
|
25
|
+
--rose: #f43f5e;
|
|
26
|
+
--violet: #a78bfa;
|
|
27
|
+
--radius-sm: 6px;
|
|
28
|
+
--radius-md: 10px;
|
|
29
|
+
--radius-lg: 14px;
|
|
30
|
+
--transition: 0.18s cubic-bezier(.4,0,.2,1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
34
|
+
html, body { width: 100%; height: 100%; overflow: hidden; }
|
|
35
|
+
|
|
36
|
+
body {
|
|
37
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'SF Pro Text', sans-serif;
|
|
38
|
+
background: var(--bg);
|
|
39
|
+
color: var(--text-1);
|
|
40
|
+
font-size: 11px;
|
|
41
|
+
line-height: 1.5;
|
|
42
|
+
-webkit-font-smoothing: antialiased;
|
|
43
|
+
user-select: none;
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
body::before {
|
|
49
|
+
content: '';
|
|
50
|
+
position: fixed;
|
|
51
|
+
top: -80px; left: -60px;
|
|
52
|
+
width: 260px; height: 260px;
|
|
53
|
+
background: radial-gradient(circle, rgba(99,102,241,0.08) 0%, transparent 70%);
|
|
54
|
+
pointer-events: none;
|
|
55
|
+
z-index: 0;
|
|
56
|
+
}
|
|
57
|
+
body::after {
|
|
58
|
+
content: '';
|
|
59
|
+
position: fixed;
|
|
60
|
+
bottom: -60px; right: -40px;
|
|
61
|
+
width: 200px; height: 200px;
|
|
62
|
+
background: radial-gradient(circle, rgba(6,182,212,0.06) 0%, transparent 70%);
|
|
63
|
+
pointer-events: none;
|
|
64
|
+
z-index: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.app {
|
|
68
|
+
position: relative;
|
|
69
|
+
z-index: 1;
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
height: 100%;
|
|
73
|
+
padding: 10px;
|
|
74
|
+
gap: 7px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* HEADER */
|
|
78
|
+
.header {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
justify-content: space-between;
|
|
82
|
+
background: var(--surface-1);
|
|
83
|
+
border: 1px solid var(--border-1);
|
|
84
|
+
border-radius: var(--radius-md);
|
|
85
|
+
padding: 8px 10px;
|
|
86
|
+
flex-shrink: 0;
|
|
87
|
+
position: relative;
|
|
88
|
+
overflow: hidden;
|
|
89
|
+
}
|
|
90
|
+
.header::before {
|
|
91
|
+
content: '';
|
|
92
|
+
position: absolute;
|
|
93
|
+
top: 0; left: 0; right: 0;
|
|
94
|
+
height: 1px;
|
|
95
|
+
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.brand { display: flex; align-items: center; gap: 9px; }
|
|
99
|
+
|
|
100
|
+
.brand-logo {
|
|
101
|
+
width: 26px; height: 26px;
|
|
102
|
+
border-radius: 7px;
|
|
103
|
+
background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%);
|
|
104
|
+
display: flex; align-items: center; justify-content: center;
|
|
105
|
+
flex-shrink: 0;
|
|
106
|
+
box-shadow: 0 0 0 1px rgba(99,102,241,0.3), 0 4px 12px rgba(79,70,229,0.35);
|
|
107
|
+
}
|
|
108
|
+
.brand-logo svg { display: block; }
|
|
109
|
+
.brand-info { display: flex; flex-direction: column; gap: 1px; }
|
|
110
|
+
.brand-name { font-size: 11.5px; font-weight: 700; letter-spacing: 0.3px; color: var(--text-1); line-height: 1; }
|
|
111
|
+
.brand-file { font-size: 9px; color: var(--text-3); font-weight: 500; letter-spacing: 0.2px; line-height: 1; }
|
|
112
|
+
|
|
113
|
+
.status-pill {
|
|
114
|
+
display: flex; align-items: center; gap: 6px;
|
|
115
|
+
background: var(--surface-2);
|
|
116
|
+
border: 1px solid var(--border-1);
|
|
117
|
+
border-radius: 100px;
|
|
118
|
+
padding: 4px 10px 4px 7px;
|
|
119
|
+
font-size: 10px; font-weight: 600;
|
|
120
|
+
color: var(--text-2); letter-spacing: 0.1px;
|
|
121
|
+
transition: border-color var(--transition), background var(--transition);
|
|
122
|
+
}
|
|
123
|
+
.status-pill.connected { border-color: rgba(34,211,160,0.25); background: rgba(34,211,160,0.06); }
|
|
124
|
+
.status-pill.offline { border-color: rgba(244,63,94,0.2); background: rgba(244,63,94,0.05); }
|
|
125
|
+
|
|
126
|
+
.dot {
|
|
127
|
+
width: 7px; height: 7px;
|
|
128
|
+
border-radius: 50%; flex-shrink: 0;
|
|
129
|
+
transition: background var(--transition), box-shadow var(--transition);
|
|
130
|
+
}
|
|
131
|
+
.dot.green { background: var(--green); box-shadow: 0 0 0 2px rgba(34,211,160,0.2), 0 0 8px rgba(34,211,160,0.5); }
|
|
132
|
+
.dot.yellow { background: var(--amber); box-shadow: 0 0 0 2px rgba(245,158,11,0.2), 0 0 8px rgba(245,158,11,0.4); animation: blink 1.4s ease-in-out infinite; }
|
|
133
|
+
.dot.red { background: var(--rose); box-shadow: 0 0 0 2px rgba(244,63,94,0.2), 0 0 8px rgba(244,63,94,0.4); }
|
|
134
|
+
|
|
135
|
+
@keyframes blink {
|
|
136
|
+
0%, 100% { opacity: 1; }
|
|
137
|
+
50% { opacity: 0.2; }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* STATS ROW */
|
|
141
|
+
.stats-row {
|
|
142
|
+
display: grid;
|
|
143
|
+
grid-template-columns: 1fr 1fr 1fr auto;
|
|
144
|
+
gap: 6px;
|
|
145
|
+
flex-shrink: 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.stat-card {
|
|
149
|
+
background: var(--surface-1);
|
|
150
|
+
border: 1px solid var(--border-1);
|
|
151
|
+
border-radius: var(--radius-sm);
|
|
152
|
+
padding: 7px 8px;
|
|
153
|
+
display: flex; flex-direction: column; gap: 3px;
|
|
154
|
+
transition: border-color var(--transition);
|
|
155
|
+
}
|
|
156
|
+
.stat-card:hover { border-color: var(--border-2); }
|
|
157
|
+
|
|
158
|
+
.stat-label {
|
|
159
|
+
font-size: 8.5px; font-weight: 700;
|
|
160
|
+
letter-spacing: 0.8px; text-transform: uppercase;
|
|
161
|
+
color: var(--text-3);
|
|
162
|
+
}
|
|
163
|
+
.stat-value {
|
|
164
|
+
font-size: 17px; font-weight: 800;
|
|
165
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
166
|
+
line-height: 1;
|
|
167
|
+
}
|
|
168
|
+
.stat-card.writes .stat-value { color: var(--violet); }
|
|
169
|
+
.stat-card.reads .stat-value { color: var(--accent-2); }
|
|
170
|
+
.stat-card.errors .stat-value { color: var(--rose); }
|
|
171
|
+
|
|
172
|
+
.port-card {
|
|
173
|
+
background: var(--surface-1);
|
|
174
|
+
border: 1px solid var(--border-1);
|
|
175
|
+
border-radius: var(--radius-sm);
|
|
176
|
+
padding: 7px 10px;
|
|
177
|
+
display: flex; flex-direction: column; gap: 3px;
|
|
178
|
+
cursor: pointer;
|
|
179
|
+
transition: border-color var(--transition), background var(--transition);
|
|
180
|
+
}
|
|
181
|
+
.port-card:hover { border-color: rgba(6,182,212,0.35); background: rgba(6,182,212,0.04); }
|
|
182
|
+
.port-label { font-size: 8.5px; font-weight: 700; letter-spacing: 0.8px; text-transform: uppercase; color: var(--text-3); }
|
|
183
|
+
.port-value { font-size: 12px; font-weight: 700; font-family: 'SF Mono', 'Fira Code', monospace; color: var(--accent-2); line-height: 1; }
|
|
184
|
+
|
|
185
|
+
/* TERMINAL */
|
|
186
|
+
.terminal-wrap {
|
|
187
|
+
flex: 1; min-height: 0;
|
|
188
|
+
display: flex; flex-direction: column;
|
|
189
|
+
background: var(--surface-1);
|
|
190
|
+
border: 1px solid var(--border-1);
|
|
191
|
+
border-radius: var(--radius-md);
|
|
192
|
+
overflow: hidden;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.terminal-header {
|
|
196
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
197
|
+
padding: 6px 10px;
|
|
198
|
+
background: var(--surface-2);
|
|
199
|
+
border-bottom: 1px solid var(--border-1);
|
|
200
|
+
flex-shrink: 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.terminal-title {
|
|
204
|
+
display: flex; align-items: center; gap: 6px;
|
|
205
|
+
font-size: 9px; font-weight: 700;
|
|
206
|
+
letter-spacing: 0.8px; text-transform: uppercase;
|
|
207
|
+
color: var(--text-3);
|
|
208
|
+
}
|
|
209
|
+
.t-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); opacity: 0.5; }
|
|
210
|
+
|
|
211
|
+
.event-badge {
|
|
212
|
+
font-size: 9px; font-weight: 600;
|
|
213
|
+
color: var(--text-3);
|
|
214
|
+
background: var(--surface-3);
|
|
215
|
+
border: 1px solid var(--border-1);
|
|
216
|
+
border-radius: 100px;
|
|
217
|
+
padding: 1px 7px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.terminal-body {
|
|
221
|
+
flex: 1;
|
|
222
|
+
padding: 8px 10px;
|
|
223
|
+
font-family: 'SF Mono', 'JetBrains Mono', 'Fira Code', monospace;
|
|
224
|
+
font-size: 10.5px; line-height: 1.6;
|
|
225
|
+
overflow-y: auto;
|
|
226
|
+
color: var(--text-2);
|
|
227
|
+
}
|
|
228
|
+
.terminal-body::-webkit-scrollbar { width: 3px; }
|
|
229
|
+
.terminal-body::-webkit-scrollbar-track { background: transparent; }
|
|
230
|
+
.terminal-body::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 2px; }
|
|
231
|
+
|
|
232
|
+
.log-line {
|
|
233
|
+
display: flex; align-items: baseline; gap: 8px;
|
|
234
|
+
padding: 1px 3px; border-radius: 3px;
|
|
235
|
+
}
|
|
236
|
+
.log-line:hover { background: rgba(255,255,255,0.02); }
|
|
237
|
+
.log-ts { font-size: 9px; color: var(--text-3); flex-shrink: 0; letter-spacing: -0.2px; }
|
|
238
|
+
.log-msg { flex: 1; word-break: break-all; }
|
|
239
|
+
.log-line .ok { color: var(--green); }
|
|
240
|
+
.log-line .err { color: var(--rose); }
|
|
241
|
+
.log-line .info { color: var(--accent-2); }
|
|
242
|
+
|
|
243
|
+
.terminal-empty {
|
|
244
|
+
display: flex; flex-direction: column;
|
|
245
|
+
align-items: center; justify-content: center;
|
|
246
|
+
height: 100%; gap: 6px; color: var(--text-3);
|
|
247
|
+
}
|
|
248
|
+
.terminal-empty-icon { font-size: 20px; opacity: 0.4; }
|
|
249
|
+
.terminal-empty-text { font-size: 10px; opacity: 0.5; }
|
|
250
|
+
|
|
251
|
+
/* FOOTER */
|
|
252
|
+
.footer { display: flex; gap: 6px; flex-shrink: 0; }
|
|
253
|
+
|
|
254
|
+
.btn {
|
|
255
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
256
|
+
gap: 5px; padding: 7px 11px;
|
|
257
|
+
border-radius: var(--radius-sm);
|
|
258
|
+
font-size: 10.5px; font-weight: 600;
|
|
259
|
+
cursor: pointer;
|
|
260
|
+
border: 1px solid var(--border-2);
|
|
261
|
+
background: var(--surface-2);
|
|
262
|
+
color: var(--text-2);
|
|
263
|
+
transition: all var(--transition);
|
|
264
|
+
letter-spacing: 0.1px;
|
|
265
|
+
white-space: nowrap;
|
|
266
|
+
}
|
|
267
|
+
.btn:hover { background: var(--surface-3); border-color: var(--border-3); color: var(--text-1); }
|
|
268
|
+
.btn:active { transform: scale(0.96); }
|
|
269
|
+
|
|
270
|
+
.btn-icon { width: 30px; height: 30px; padding: 0; flex-shrink: 0; }
|
|
271
|
+
|
|
272
|
+
.btn-primary {
|
|
273
|
+
flex: 1;
|
|
274
|
+
background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%);
|
|
275
|
+
border-color: transparent; color: #fff; font-weight: 700;
|
|
276
|
+
box-shadow: 0 1px 0 rgba(255,255,255,0.1) inset, 0 4px 16px rgba(79,70,229,0.3);
|
|
277
|
+
}
|
|
278
|
+
.btn-primary:hover {
|
|
279
|
+
opacity: 0.9; border-color: transparent; color: #fff;
|
|
280
|
+
box-shadow: 0 1px 0 rgba(255,255,255,0.12) inset, 0 6px 20px rgba(79,70,229,0.4);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.btn-live {
|
|
284
|
+
flex: 1;
|
|
285
|
+
background: rgba(34,211,160,0.08);
|
|
286
|
+
border-color: rgba(34,211,160,0.25);
|
|
287
|
+
color: var(--green);
|
|
288
|
+
}
|
|
289
|
+
.btn-live:hover { background: rgba(34,211,160,0.12); border-color: rgba(34,211,160,0.4); color: var(--green); }
|
|
290
|
+
|
|
291
|
+
/* RESIZE GRIP */
|
|
292
|
+
.resize-grip {
|
|
293
|
+
position: fixed; right: 3px; bottom: 3px;
|
|
294
|
+
width: 14px; height: 14px;
|
|
295
|
+
cursor: nwse-resize; opacity: 0.2;
|
|
296
|
+
color: var(--text-2);
|
|
297
|
+
transition: opacity var(--transition), color var(--transition);
|
|
298
|
+
z-index: 1000;
|
|
299
|
+
display: flex; align-items: flex-end; justify-content: flex-end;
|
|
300
|
+
}
|
|
301
|
+
.resize-grip:hover { opacity: 0.7; color: var(--accent-2); }
|
|
302
|
+
</style>
|
|
303
|
+
</head>
|
|
304
|
+
<body>
|
|
305
|
+
|
|
306
|
+
<div class="app">
|
|
307
|
+
|
|
308
|
+
<header class="header">
|
|
309
|
+
<div class="brand">
|
|
310
|
+
<div class="brand-logo">
|
|
311
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
312
|
+
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
|
313
|
+
</svg>
|
|
314
|
+
</div>
|
|
315
|
+
<div class="brand-info">
|
|
316
|
+
<span class="brand-name">Figma MCP</span>
|
|
317
|
+
<span class="brand-file" id="session-display">READY</span>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<div class="status-pill" id="status-pill">
|
|
322
|
+
<div class="dot yellow" id="dot"></div>
|
|
323
|
+
<span id="status-text">Connecting</span>
|
|
324
|
+
<span id="mode-badge" style="display:none;color:var(--text-3)">—</span>
|
|
325
|
+
</div>
|
|
326
|
+
</header>
|
|
327
|
+
|
|
328
|
+
<div class="stats-row">
|
|
329
|
+
<div class="stat-card writes" title="Write Operations">
|
|
330
|
+
<span class="stat-label">Writes</span>
|
|
331
|
+
<span class="stat-value" id="write-count">0</span>
|
|
332
|
+
</div>
|
|
333
|
+
<div class="stat-card reads" title="Read Operations">
|
|
334
|
+
<span class="stat-label">Reads</span>
|
|
335
|
+
<span class="stat-value" id="read-count">0</span>
|
|
336
|
+
</div>
|
|
337
|
+
<div class="stat-card errors" title="Errors">
|
|
338
|
+
<span class="stat-label">Errors</span>
|
|
339
|
+
<span class="stat-value" id="err-count">0</span>
|
|
340
|
+
</div>
|
|
341
|
+
<div class="port-card" onclick="changePort()" title="Click to change port">
|
|
342
|
+
<span class="port-label">Port</span>
|
|
343
|
+
<span class="port-value" id="port-display">38451</span>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<section class="terminal-wrap">
|
|
348
|
+
<div class="terminal-header">
|
|
349
|
+
<div class="terminal-title">
|
|
350
|
+
<div class="t-dot"></div>
|
|
351
|
+
Stream Console
|
|
352
|
+
</div>
|
|
353
|
+
<span class="event-badge" id="log-count">0 events</span>
|
|
354
|
+
</div>
|
|
355
|
+
<div class="terminal-body" id="log">
|
|
356
|
+
<div class="terminal-empty" id="log-empty">
|
|
357
|
+
<div class="terminal-empty-icon">◌</div>
|
|
358
|
+
<div class="terminal-empty-text">Waiting for events…</div>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
</section>
|
|
362
|
+
|
|
363
|
+
<footer class="footer">
|
|
364
|
+
<button class="btn btn-icon" onclick="clearLog()" title="Clear log">
|
|
365
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
366
|
+
<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/>
|
|
367
|
+
</svg>
|
|
368
|
+
</button>
|
|
369
|
+
|
|
370
|
+
<button class="btn btn-primary" id="launch-btn" onclick="launchServer()" title="Start Figma MCP server">
|
|
371
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
|
372
|
+
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
|
373
|
+
</svg>
|
|
374
|
+
<span id="launch-btn-label">Launch Server</span>
|
|
375
|
+
</button>
|
|
376
|
+
|
|
377
|
+
<button class="btn btn-icon" onclick="reconnect()" title="Reconnect">
|
|
378
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
379
|
+
<polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
|
380
|
+
</svg>
|
|
381
|
+
</button>
|
|
382
|
+
</footer>
|
|
383
|
+
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
<div class="resize-grip" id="resize-grip" title="Drag to resize">
|
|
387
|
+
<svg width="9" height="9" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
|
388
|
+
<path d="M9 1L1 9M9 5L5 9"/>
|
|
389
|
+
</svg>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<script>
|
|
393
|
+
var currentPort = 38451;
|
|
394
|
+
var BRIDGE = "http://localhost:" + currentPort;
|
|
395
|
+
var POLL_MS = 300;
|
|
396
|
+
var POLL_MAX_MS = 3000;
|
|
397
|
+
var LONG_POLL_TIMEOUT = 12000;
|
|
398
|
+
var SHORT_POLL_TIMEOUT = 3000;
|
|
399
|
+
var consecutiveErrors = 0;
|
|
400
|
+
var everConnected = false;
|
|
401
|
+
var lastSuccessfulPollAt = Date.now();
|
|
402
|
+
var sessionId = null;
|
|
403
|
+
var fileName = null;
|
|
404
|
+
|
|
405
|
+
var writeCount = 0, readCount = 0, errCount = 0;
|
|
406
|
+
var totalLogEvents = 0;
|
|
407
|
+
var polling = true;
|
|
408
|
+
|
|
409
|
+
var logEl = document.getElementById("log");
|
|
410
|
+
var logEmptyEl = document.getElementById("log-empty");
|
|
411
|
+
var dotEl = document.getElementById("dot");
|
|
412
|
+
var statusEl = document.getElementById("status-text");
|
|
413
|
+
var modeEl = document.getElementById("mode-badge");
|
|
414
|
+
var logCountEl = document.getElementById("log-count");
|
|
415
|
+
var sessionDispEl = document.getElementById("session-display");
|
|
416
|
+
var launchBtn = document.getElementById("launch-btn");
|
|
417
|
+
var launchBtnLabel = document.getElementById("launch-btn-label");
|
|
418
|
+
var statusPill = document.getElementById("status-pill");
|
|
419
|
+
|
|
420
|
+
var READ_OPS = [
|
|
421
|
+
"get_selection","get_design","get_page_nodes","screenshot",
|
|
422
|
+
"get_styles","get_local_components","get_viewport","get_variables",
|
|
423
|
+
"export_svg","get_node_detail","export_image","search_nodes","scan_design",
|
|
424
|
+
"index_scan"
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
function setStatus(state, text, mode) {
|
|
428
|
+
dotEl.className = "dot " + state;
|
|
429
|
+
statusEl.textContent = text;
|
|
430
|
+
if (mode && modeEl) modeEl.textContent = mode;
|
|
431
|
+
|
|
432
|
+
statusPill.className = "status-pill";
|
|
433
|
+
if (state === "green") statusPill.classList.add("connected");
|
|
434
|
+
if (state === "red") statusPill.classList.add("offline");
|
|
435
|
+
|
|
436
|
+
if (launchBtn && launchBtnLabel) {
|
|
437
|
+
launchBtn.className = "btn";
|
|
438
|
+
if (state === "green") {
|
|
439
|
+
launchBtn.classList.add("btn-live");
|
|
440
|
+
launchBtnLabel.textContent = "● Live";
|
|
441
|
+
} else {
|
|
442
|
+
launchBtn.classList.add("btn-primary");
|
|
443
|
+
launchBtnLabel.textContent = state === "yellow" ? "Connecting…" : "Launch Server";
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function launchServer() {
|
|
449
|
+
if (wsConnected) {
|
|
450
|
+
log("Server is already running and connected!", "ok");
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
showSetupGuide();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function showSetupGuide() {
|
|
457
|
+
var existing = document.getElementById("setup-overlay");
|
|
458
|
+
if (existing) { existing.remove(); return; }
|
|
459
|
+
|
|
460
|
+
var overlay = document.createElement("div");
|
|
461
|
+
overlay.id = "setup-overlay";
|
|
462
|
+
Object.assign(overlay.style, {
|
|
463
|
+
position: "fixed", inset: "0",
|
|
464
|
+
background: "rgba(9,9,15,0.96)",
|
|
465
|
+
backdropFilter: "blur(4px)",
|
|
466
|
+
zIndex: "999",
|
|
467
|
+
display: "flex", flexDirection: "column",
|
|
468
|
+
alignItems: "center", justifyContent: "center",
|
|
469
|
+
padding: "18px", gap: "10px",
|
|
470
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Inter', sans-serif"
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
overlay.innerHTML = [
|
|
474
|
+
'<div style="width:44px;height:44px;border-radius:12px;background:linear-gradient(135deg,#4f46e5,#06b6d4);display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 1px rgba(99,102,241,0.4),0 8px 24px rgba(79,70,229,0.4);margin-bottom:2px">',
|
|
475
|
+
'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>',
|
|
476
|
+
'</div>',
|
|
477
|
+
'<div style="font-size:14px;font-weight:700;color:#f0f2f8;letter-spacing:-0.2px">Start Figma MCP Server</div>',
|
|
478
|
+
'<div style="font-size:10.5px;color:#8b95b0;text-align:center;max-width:230px;line-height:1.6">Run one of these commands in your terminal:</div>',
|
|
479
|
+
|
|
480
|
+
'<div style="width:100%;background:#0f1117;border:1px solid rgba(255,255,255,0.07);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:4px">',
|
|
481
|
+
'<div style="font-size:8.5px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:#4e5873">Option A · Run once</div>',
|
|
482
|
+
'<div style="font-family:\'SF Mono\',monospace;font-size:12px;font-weight:600;color:#38bdf8;user-select:text">npx figma-mcp</div>',
|
|
483
|
+
'</div>',
|
|
484
|
+
|
|
485
|
+
'<div style="width:100%;background:#0f1117;border:1px solid rgba(34,211,160,0.15);border-radius:10px;padding:10px 12px;display:flex;flex-direction:column;gap:4px">',
|
|
486
|
+
'<div style="font-size:8.5px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:#4e5873">Option B · Auto-start on login ✦ Recommended</div>',
|
|
487
|
+
'<div style="font-family:\'SF Mono\',monospace;font-size:12px;font-weight:600;color:#22d3a0;user-select:text">npx figma-mcp --install-service</div>',
|
|
488
|
+
'<div style="font-size:9px;color:#4e5873">Sets up LaunchAgent / systemd / Task Scheduler</div>',
|
|
489
|
+
'</div>',
|
|
490
|
+
|
|
491
|
+
'<div style="display:flex;align-items:center;gap:6px;font-size:9.5px;color:#4e5873">',
|
|
492
|
+
'<div style="width:5px;height:5px;border-radius:50%;background:#f59e0b;animation:blink 1.4s ease-in-out infinite"></div>',
|
|
493
|
+
'Plugin auto-connects once server is detected',
|
|
494
|
+
'</div>',
|
|
495
|
+
|
|
496
|
+
'<button onclick="document.getElementById(\'setup-overlay\').remove();reconnect();" style="',
|
|
497
|
+
'margin-top:4px;padding:7px 20px;border-radius:8px;border:1px solid rgba(255,255,255,0.1);',
|
|
498
|
+
'background:rgba(255,255,255,0.05);color:#8b95b0;font-size:10.5px;font-weight:600;cursor:pointer;',
|
|
499
|
+
'font-family:inherit;letter-spacing:0.1px',
|
|
500
|
+
'">Dismiss & Retry</button>'
|
|
501
|
+
].join("");
|
|
502
|
+
|
|
503
|
+
document.body.appendChild(overlay);
|
|
504
|
+
|
|
505
|
+
var checks = 0;
|
|
506
|
+
var checker = setInterval(function() {
|
|
507
|
+
checks++;
|
|
508
|
+
fetch(BRIDGE + "/health", { cache: "no-store", signal: AbortSignal.timeout(400) })
|
|
509
|
+
.then(function(res) {
|
|
510
|
+
if (res.ok) {
|
|
511
|
+
clearInterval(checker);
|
|
512
|
+
var ov = document.getElementById("setup-overlay");
|
|
513
|
+
if (ov) ov.remove();
|
|
514
|
+
log("✓ Server detected — connecting…", "ok");
|
|
515
|
+
reconnect();
|
|
516
|
+
}
|
|
517
|
+
})
|
|
518
|
+
.catch(function() {
|
|
519
|
+
if (checks >= 60) clearInterval(checker);
|
|
520
|
+
});
|
|
521
|
+
}, 500);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function log(msg, cls) {
|
|
525
|
+
totalLogEvents++;
|
|
526
|
+
if (logCountEl) logCountEl.textContent = totalLogEvents + " events";
|
|
527
|
+
|
|
528
|
+
if (logEmptyEl) logEmptyEl.style.display = "none";
|
|
529
|
+
|
|
530
|
+
var line = document.createElement("div");
|
|
531
|
+
line.className = "log-line";
|
|
532
|
+
|
|
533
|
+
var ts = document.createElement("span");
|
|
534
|
+
ts.className = "log-ts";
|
|
535
|
+
ts.textContent = new Date().toLocaleTimeString("en", { hour12: false });
|
|
536
|
+
|
|
537
|
+
var content = document.createElement("span");
|
|
538
|
+
content.className = "log-msg" + (cls ? " " + cls : "");
|
|
539
|
+
content.textContent = msg;
|
|
540
|
+
|
|
541
|
+
line.appendChild(ts);
|
|
542
|
+
line.appendChild(content);
|
|
543
|
+
logEl.appendChild(line);
|
|
544
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
545
|
+
|
|
546
|
+
// Trim log buffer: remove oldest entries in one batch DOM op to avoid
|
|
547
|
+
// triggering O(N) reflows from individual removeChild calls.
|
|
548
|
+
if (logEl.children.length > 90) {
|
|
549
|
+
var keep = Array.from(logEl.children).slice(-70);
|
|
550
|
+
logEl.replaceChildren.apply(logEl, keep);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function clearLog() {
|
|
555
|
+
logEl.innerHTML = "";
|
|
556
|
+
totalLogEvents = 0;
|
|
557
|
+
if (logCountEl) logCountEl.textContent = "0 events";
|
|
558
|
+
var empty = document.createElement("div");
|
|
559
|
+
empty.id = "log-empty";
|
|
560
|
+
empty.className = "terminal-empty";
|
|
561
|
+
empty.innerHTML = '<div class="terminal-empty-icon">◌</div><div class="terminal-empty-text">Waiting for events…</div>';
|
|
562
|
+
logEl.appendChild(empty);
|
|
563
|
+
logEmptyEl = empty;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function sleep(ms) {
|
|
567
|
+
return new Promise(function(r) { setTimeout(r, ms); });
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function getBackoff() {
|
|
571
|
+
return Math.min(POLL_MS * Math.pow(2, Math.min(consecutiveErrors - 1, 3)), POLL_MAX_MS);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
var ws = null;
|
|
575
|
+
var wsConnected = false;
|
|
576
|
+
var isLongPolling = false;
|
|
577
|
+
var activeAbortController = null;
|
|
578
|
+
var isFirstPoll = true;
|
|
579
|
+
|
|
580
|
+
var retryTimer = null;
|
|
581
|
+
|
|
582
|
+
async function initConnection() {
|
|
583
|
+
if (wsConnected) return;
|
|
584
|
+
|
|
585
|
+
// 1. Instant Health Check on localhost (< 2ms)
|
|
586
|
+
try {
|
|
587
|
+
var res = await fetch("http://localhost:" + currentPort + "/health", { signal: AbortSignal.timeout(600) });
|
|
588
|
+
if (res.ok) {
|
|
589
|
+
BRIDGE = "http://localhost:" + currentPort;
|
|
590
|
+
everConnected = true;
|
|
591
|
+
consecutiveErrors = 0;
|
|
592
|
+
lastSuccessfulPollAt = Date.now();
|
|
593
|
+
setStatus("green", "Connected", "ready");
|
|
594
|
+
connectWs();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
} catch(e) {}
|
|
598
|
+
|
|
599
|
+
// 2. Parallel port scan 38451-38455 — all probes fire simultaneously
|
|
600
|
+
var scanPorts = [38451, 38452, 38453, 38454, 38455].filter(function(p) { return p !== currentPort; });
|
|
601
|
+
try {
|
|
602
|
+
var winner = await Promise.any(scanPorts.map(function(p) {
|
|
603
|
+
return fetch("http://localhost:" + p + "/health", { signal: AbortSignal.timeout(500) })
|
|
604
|
+
.then(function(r) { if (!r.ok) throw new Error("not ok"); return p; });
|
|
605
|
+
}));
|
|
606
|
+
currentPort = winner;
|
|
607
|
+
BRIDGE = "http://localhost:" + winner;
|
|
608
|
+
var pDisp = document.getElementById("port-display");
|
|
609
|
+
if (pDisp) pDisp.textContent = winner;
|
|
610
|
+
everConnected = true;
|
|
611
|
+
consecutiveErrors = 0;
|
|
612
|
+
lastSuccessfulPollAt = Date.now();
|
|
613
|
+
setStatus("green", "Connected", "ready");
|
|
614
|
+
connectWs();
|
|
615
|
+
return;
|
|
616
|
+
} catch(_) { /* no server yet */ }
|
|
617
|
+
|
|
618
|
+
// 3. If server is not yet running, schedule continuous auto-detection every 1.5s
|
|
619
|
+
setStatus("yellow", "Waiting for server…");
|
|
620
|
+
if (retryTimer) clearTimeout(retryTimer);
|
|
621
|
+
retryTimer = setTimeout(function() {
|
|
622
|
+
if (!wsConnected && polling) {
|
|
623
|
+
initConnection();
|
|
624
|
+
}
|
|
625
|
+
}, 1500);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function connectWs() {
|
|
629
|
+
if (ws) {
|
|
630
|
+
try { ws.close(); } catch(e) {}
|
|
631
|
+
ws = null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
var wsHost = BRIDGE.replace(/^http:\/\//, "ws://");
|
|
635
|
+
var wsUrl = wsHost + "/ws";
|
|
636
|
+
var qs = [];
|
|
637
|
+
if (sessionId) qs.push("sessionId=" + encodeURIComponent(sessionId));
|
|
638
|
+
if (fileName) qs.push("fileName=" + encodeURIComponent(fileName));
|
|
639
|
+
if (qs.length) wsUrl += "?" + qs.join("&");
|
|
640
|
+
|
|
641
|
+
try {
|
|
642
|
+
ws = new WebSocket(wsUrl);
|
|
643
|
+
} catch (err) {
|
|
644
|
+
startLongPoll();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
var pingTimer = null;
|
|
649
|
+
|
|
650
|
+
ws.onopen = function() {
|
|
651
|
+
wsConnected = true;
|
|
652
|
+
consecutiveErrors = 0;
|
|
653
|
+
everConnected = true;
|
|
654
|
+
if (retryTimer) clearTimeout(retryTimer);
|
|
655
|
+
lastSuccessfulPollAt = Date.now();
|
|
656
|
+
setStatus("green", "Connected", "ready");
|
|
657
|
+
log("⚡ Connected to Figma MCP · port " + currentPort, "ok");
|
|
658
|
+
|
|
659
|
+
if (pingTimer) clearInterval(pingTimer);
|
|
660
|
+
pingTimer = setInterval(function() {
|
|
661
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
662
|
+
try { ws.send(JSON.stringify({ type: "ping" })); } catch(e) {}
|
|
663
|
+
} else {
|
|
664
|
+
clearInterval(pingTimer);
|
|
665
|
+
}
|
|
666
|
+
}, 15000);
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
ws.onmessage = function(event) {
|
|
670
|
+
try {
|
|
671
|
+
var req = JSON.parse(event.data);
|
|
672
|
+
if (req && req.operation) {
|
|
673
|
+
log("→ " + req.operation, READ_OPS.indexOf(req.operation) >= 0 ? "info" : "ok");
|
|
674
|
+
try { ws.send(JSON.stringify({ type: "ack", id: req.id })); } catch(e) {}
|
|
675
|
+
dispatchToMain(req);
|
|
676
|
+
}
|
|
677
|
+
} catch (e) {
|
|
678
|
+
log("WS parse error: " + e.message, "err");
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
ws.onerror = function() {
|
|
683
|
+
if (!everConnected) {
|
|
684
|
+
setStatus("yellow", "Connecting…");
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
ws.onclose = function() {
|
|
689
|
+
if (pingTimer) clearInterval(pingTimer);
|
|
690
|
+
wsConnected = false;
|
|
691
|
+
if (polling) {
|
|
692
|
+
// Attempt long-poll fallback and schedule retry
|
|
693
|
+
startLongPoll();
|
|
694
|
+
if (retryTimer) clearTimeout(retryTimer);
|
|
695
|
+
retryTimer = setTimeout(function() {
|
|
696
|
+
if (!wsConnected && polling) initConnection();
|
|
697
|
+
}, 2000);
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function startLongPoll() {
|
|
703
|
+
if (isLongPolling || wsConnected) return;
|
|
704
|
+
isLongPolling = true;
|
|
705
|
+
|
|
706
|
+
try {
|
|
707
|
+
while (polling && !wsConnected) {
|
|
708
|
+
var receivedThisRound = 0;
|
|
709
|
+
try {
|
|
710
|
+
var pollUrl = BRIDGE + "/poll";
|
|
711
|
+
var qs = [];
|
|
712
|
+
if (sessionId) qs.push("sessionId=" + encodeURIComponent(sessionId));
|
|
713
|
+
if (fileName) qs.push("fileName=" + encodeURIComponent(fileName));
|
|
714
|
+
if (isFirstPoll) {
|
|
715
|
+
qs.push("init=true");
|
|
716
|
+
isFirstPoll = false;
|
|
717
|
+
}
|
|
718
|
+
if (qs.length) pollUrl += "?" + qs.join("&");
|
|
719
|
+
|
|
720
|
+
var fetchTimeout = consecutiveErrors > 0 ? SHORT_POLL_TIMEOUT : LONG_POLL_TIMEOUT;
|
|
721
|
+
var ctrl = new AbortController();
|
|
722
|
+
activeAbortController = ctrl;
|
|
723
|
+
var timeoutId = setTimeout(function() { ctrl.abort(); }, fetchTimeout);
|
|
724
|
+
var res;
|
|
725
|
+
try {
|
|
726
|
+
res = await fetch(pollUrl, { signal: ctrl.signal });
|
|
727
|
+
} finally {
|
|
728
|
+
clearTimeout(timeoutId);
|
|
729
|
+
if (activeAbortController === ctrl) {
|
|
730
|
+
activeAbortController = null;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (!res.ok) {
|
|
735
|
+
setStatus("yellow", "Busy");
|
|
736
|
+
consecutiveErrors++;
|
|
737
|
+
await sleep(getBackoff());
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
var body = await res.json();
|
|
742
|
+
var reqs = body.requests || [];
|
|
743
|
+
receivedThisRound = reqs.length;
|
|
744
|
+
setStatus("green", reqs.length ? reqs.length + " cmd" : "Connected", body.mode || "ready");
|
|
745
|
+
|
|
746
|
+
consecutiveErrors = 0;
|
|
747
|
+
everConnected = true;
|
|
748
|
+
lastSuccessfulPollAt = Date.now();
|
|
749
|
+
|
|
750
|
+
for (var i = 0; i < reqs.length; i++) {
|
|
751
|
+
var req = reqs[i];
|
|
752
|
+
log("→ " + req.operation, READ_OPS.indexOf(req.operation) >= 0 ? "info" : "ok");
|
|
753
|
+
dispatchToMain(req);
|
|
754
|
+
}
|
|
755
|
+
} catch (e) {
|
|
756
|
+
if (!polling) break;
|
|
757
|
+
consecutiveErrors++;
|
|
758
|
+
var backoff = getBackoff();
|
|
759
|
+
if (consecutiveErrors <= 5) {
|
|
760
|
+
setStatus("yellow", "Connecting (" + consecutiveErrors + ")");
|
|
761
|
+
} else if (consecutiveErrors <= 10) {
|
|
762
|
+
setStatus("yellow", "Retrying…");
|
|
763
|
+
} else {
|
|
764
|
+
setStatus("red", "Offline");
|
|
765
|
+
}
|
|
766
|
+
await sleep(backoff);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
// Re-poll immediately after a round that carried work: the next op in
|
|
770
|
+
// a read chain is usually already queued, and sleeping here added
|
|
771
|
+
// POLL_MS of dead time to every single round-trip.
|
|
772
|
+
if (!receivedThisRound) await sleep(POLL_MS);
|
|
773
|
+
}
|
|
774
|
+
} finally {
|
|
775
|
+
isLongPolling = false;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// dataJson is already-serialized JSON produced by the main thread, so it is
|
|
780
|
+
// spliced into the body as-is — a multi-MB design tree is never re-encoded
|
|
781
|
+
// here. Older builds of code.js send a plain `data` object instead.
|
|
782
|
+
function buildResponseBody(id, success, dataJson, error) {
|
|
783
|
+
return '{"id":' + JSON.stringify(id) +
|
|
784
|
+
',"success":' + (success ? "true" : "false") +
|
|
785
|
+
',"data":' + (dataJson === undefined || dataJson === null ? "null" : dataJson) +
|
|
786
|
+
',"error":' + (error === undefined || error === null ? "null" : JSON.stringify(String(error))) +
|
|
787
|
+
'}';
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
async function sendResponse(id, success, dataJson, error) {
|
|
791
|
+
var body = buildResponseBody(id, success, dataJson, error);
|
|
792
|
+
|
|
793
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
794
|
+
try {
|
|
795
|
+
ws.send(body);
|
|
796
|
+
return;
|
|
797
|
+
} catch(e) {}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// HTTP fallback
|
|
801
|
+
try {
|
|
802
|
+
await fetch(BRIDGE + "/response", {
|
|
803
|
+
method: "POST",
|
|
804
|
+
headers: { "Content-Type": "application/json" },
|
|
805
|
+
body: body
|
|
806
|
+
});
|
|
807
|
+
} catch (e) {
|
|
808
|
+
log("Response failed: " + e.message, "err");
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function dispatchToMain(req) {
|
|
813
|
+
parent.postMessage({ pluginMessage: req }, "*");
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function reconnect() {
|
|
817
|
+
polling = false;
|
|
818
|
+
isFirstPoll = true;
|
|
819
|
+
consecutiveErrors = 0;
|
|
820
|
+
setStatus("yellow", "Reconnecting…");
|
|
821
|
+
if (ws) {
|
|
822
|
+
try { ws.close(); } catch(e) {}
|
|
823
|
+
ws = null;
|
|
824
|
+
}
|
|
825
|
+
if (activeAbortController) {
|
|
826
|
+
try { activeAbortController.abort(); } catch(e) {}
|
|
827
|
+
}
|
|
828
|
+
setTimeout(function() {
|
|
829
|
+
polling = true;
|
|
830
|
+
initConnection();
|
|
831
|
+
}, 100);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
document.addEventListener("visibilitychange", function() {
|
|
835
|
+
if (document.visibilityState !== "visible" || !everConnected) return;
|
|
836
|
+
var idleMs = Date.now() - lastSuccessfulPollAt;
|
|
837
|
+
if (!wsConnected || consecutiveErrors > 0 || activeAbortController || idleMs > 20000) {
|
|
838
|
+
log("Focus sync (" + Math.round(idleMs / 1000) + "s)", "info");
|
|
839
|
+
reconnect();
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
window.addEventListener("focus", function() {
|
|
844
|
+
if (!everConnected) return;
|
|
845
|
+
var idleMs = Date.now() - lastSuccessfulPollAt;
|
|
846
|
+
if (!wsConnected || consecutiveErrors > 0 || activeAbortController || idleMs > 20000) {
|
|
847
|
+
log("Window focus sync", "info");
|
|
848
|
+
reconnect();
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
function changePort() {
|
|
853
|
+
var input = prompt("MCP Bridge Port:", currentPort);
|
|
854
|
+
if (!input) return;
|
|
855
|
+
var newPort = parseInt(input, 10);
|
|
856
|
+
if (isNaN(newPort) || newPort < 1024 || newPort > 65535) {
|
|
857
|
+
alert("Invalid port number");
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
setPort(newPort);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function setPort(port) {
|
|
864
|
+
currentPort = port;
|
|
865
|
+
BRIDGE = "http://localhost:" + currentPort;
|
|
866
|
+
document.getElementById("port-display").textContent = currentPort;
|
|
867
|
+
log("Port switched → " + currentPort, "info");
|
|
868
|
+
reconnect();
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async function autoDetectPort() {
|
|
872
|
+
setStatus("yellow", "Scanning…");
|
|
873
|
+
log("Scanning 38451-38460…", "info");
|
|
874
|
+
for (var p = 38451; p <= 38460; p++) {
|
|
875
|
+
try {
|
|
876
|
+
var res = await fetch("http://localhost:" + p + "/health", { signal: AbortSignal.timeout(400) });
|
|
877
|
+
if (res.ok) {
|
|
878
|
+
var data = await res.json();
|
|
879
|
+
log("Detected port " + p, "ok");
|
|
880
|
+
setPort(p);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
} catch(e) {}
|
|
884
|
+
}
|
|
885
|
+
log("Not found on 38451-38460 → Reset 38451", "err");
|
|
886
|
+
setPort(38451);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
window.onmessage = async function(event) {
|
|
890
|
+
var msg = event.data.pluginMessage;
|
|
891
|
+
if (!msg) return;
|
|
892
|
+
|
|
893
|
+
if (msg.type === "session-info") {
|
|
894
|
+
var prevSid = sessionId;
|
|
895
|
+
sessionId = msg.sessionId || null;
|
|
896
|
+
fileName = msg.fileName || null;
|
|
897
|
+
if (fileName && sessionDispEl) {
|
|
898
|
+
sessionDispEl.textContent = fileName.length > 16 ? fileName.slice(0, 14) + ".." : fileName;
|
|
899
|
+
}
|
|
900
|
+
if (sessionId) log("Linked: " + (fileName || sessionId), "info");
|
|
901
|
+
if (sessionId && sessionId !== prevSid) {
|
|
902
|
+
reconnect();
|
|
903
|
+
}
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (msg.type === "selection-change") {
|
|
908
|
+
if (sessionDispEl) {
|
|
909
|
+
if (msg.count === 0) {
|
|
910
|
+
sessionDispEl.textContent = fileName ? (fileName.length > 16 ? fileName.slice(0, 14) + ".." : fileName) : "READY";
|
|
911
|
+
} else if (msg.count === 1) {
|
|
912
|
+
var nName = msg.selection[0].name;
|
|
913
|
+
sessionDispEl.textContent = "Sel: " + (nName.length > 12 ? nName.slice(0, 10) + ".." : nName);
|
|
914
|
+
} else {
|
|
915
|
+
sessionDispEl.textContent = msg.count + " layers selected";
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (msg.type === "document-change") {
|
|
922
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
923
|
+
try { ws.send(JSON.stringify({ type: "document-change" })); } catch(e) {}
|
|
924
|
+
}
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (msg.type === "index-update") {
|
|
929
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
930
|
+
try {
|
|
931
|
+
ws.send(JSON.stringify({
|
|
932
|
+
type: "index-update",
|
|
933
|
+
data: msg.data,
|
|
934
|
+
fileName: msg.fileName,
|
|
935
|
+
sessionId: msg.sessionId,
|
|
936
|
+
startMs: msg.startMs
|
|
937
|
+
}));
|
|
938
|
+
log("⚡ Pre-indexed file data", "info");
|
|
939
|
+
} catch(e) {}
|
|
940
|
+
}
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
var isRead = READ_OPS.indexOf(msg.operation) >= 0;
|
|
945
|
+
if (msg.success) {
|
|
946
|
+
if (isRead) {
|
|
947
|
+
readCount++;
|
|
948
|
+
document.getElementById("read-count").textContent = readCount;
|
|
949
|
+
} else {
|
|
950
|
+
writeCount++;
|
|
951
|
+
document.getElementById("write-count").textContent = writeCount;
|
|
952
|
+
}
|
|
953
|
+
log("✓ " + (msg.id ? msg.id.slice(-6) : "ok"), "ok");
|
|
954
|
+
} else {
|
|
955
|
+
errCount++;
|
|
956
|
+
document.getElementById("err-count").textContent = errCount;
|
|
957
|
+
log("✗ " + (msg.id ? msg.id.slice(-6) : "err") + ": " + (msg.error || "fail"), "err");
|
|
958
|
+
}
|
|
959
|
+
var payloadJson = msg.dataJson !== undefined
|
|
960
|
+
? msg.dataJson
|
|
961
|
+
: (msg.data !== undefined ? JSON.stringify(msg.data) : null);
|
|
962
|
+
await sendResponse(msg.id, msg.success, payloadJson, msg.error);
|
|
963
|
+
};
|
|
964
|
+
|
|
965
|
+
initConnection();
|
|
966
|
+
|
|
967
|
+
// Smooth Drag Resize
|
|
968
|
+
(function initResize() {
|
|
969
|
+
var grip = document.getElementById("resize-grip");
|
|
970
|
+
if (!grip) return;
|
|
971
|
+
|
|
972
|
+
grip.addEventListener("pointerdown", function(e) {
|
|
973
|
+
e.preventDefault();
|
|
974
|
+
grip.setPointerCapture(e.pointerId);
|
|
975
|
+
var startX = e.clientX;
|
|
976
|
+
var startY = e.clientY;
|
|
977
|
+
var startW = window.innerWidth;
|
|
978
|
+
var startH = window.innerHeight;
|
|
979
|
+
|
|
980
|
+
function onPointerMove(ev) {
|
|
981
|
+
var nw = Math.max(280, Math.min(1000, Math.round(startW + (ev.clientX - startX))));
|
|
982
|
+
var nh = Math.max(220, Math.min(1200, Math.round(startH + (ev.clientY - startY))));
|
|
983
|
+
parent.postMessage({ pluginMessage: { type: "resize", width: nw, height: nh } }, "*");
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function onPointerUp(ev) {
|
|
987
|
+
try { grip.releasePointerCapture(ev.pointerId); } catch(err) {}
|
|
988
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
989
|
+
window.removeEventListener("pointerup", onPointerUp);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
993
|
+
window.addEventListener("pointerup", onPointerUp);
|
|
994
|
+
});
|
|
995
|
+
})();
|
|
996
|
+
</script>
|
|
997
|
+
</body>
|
|
998
|
+
</html>
|