figma-rust-mcp 2.8.4 → 2.9.5

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/plugin/ui.html CHANGED
@@ -5,61 +5,74 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Figma MCP</title>
7
7
  <style>
8
- /* ── DESIGN TOKENS ── */
8
+ /* ── GHOSTTY DESIGN SYSTEM (High-Performance Native Terminal Aesthetic) ── */
9
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);
10
+ --bg: #0d0e12;
11
+ --surface-base: #12141a;
12
+ --surface-card: #171922;
13
+ --surface-hover: #1d202b;
14
+ --surface-active: #242837;
15
+
16
+ --border-subtle: rgba(255, 255, 255, 0.07);
17
+ --border-light: rgba(255, 255, 255, 0.13);
18
+ --border-ghost: rgba(147, 129, 255, 0.35);
19
+
20
+ --text-primary: #f4f4f7;
21
+ --text-secondary: #9aa1b2;
22
+ --text-muted: #52596d;
23
+ --text-dim: #363c4e;
24
+
25
+ /* Ghostty Signature Palette */
26
+ --ghost-lavender: #9381ff;
27
+ --ghost-glow: rgba(147, 129, 255, 0.22);
28
+ --ghost-mint: #34d399;
29
+ --ghost-mint-glow:rgba(52, 211, 153, 0.20);
30
+ --ghost-cyan: #38bdf8;
31
+ --ghost-peach: #ff9f68;
32
+ --ghost-rose: #fb7185;
33
+ --ghost-amber: #fbbf24;
34
+
35
+ --radius-xs: 4px;
36
+ --radius-sm: 6px;
37
+ --radius-md: 9px;
38
+ --radius-full: 9999px;
39
+
40
+ --font-ui: -apple-system, BlinkMacSystemFont, 'Pretendard', 'Inter', 'SF Pro Text', sans-serif;
41
+ --font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', Menlo, monospace;
42
+ --transition: 0.14s cubic-bezier(0.4, 0, 0.2, 1);
31
43
  }
32
44
 
33
45
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
34
46
  html, body { width: 100%; height: 100%; overflow: hidden; }
35
47
 
36
48
  body {
37
- font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'SF Pro Text', sans-serif;
49
+ font-family: var(--font-ui);
38
50
  background: var(--bg);
39
- color: var(--text-1);
51
+ color: var(--text-primary);
40
52
  font-size: 11px;
41
- line-height: 1.5;
53
+ line-height: 1.4;
42
54
  -webkit-font-smoothing: antialiased;
43
55
  user-select: none;
44
56
  display: flex;
45
57
  flex-direction: column;
46
58
  }
47
59
 
60
+ /* Ghostty subtle atmospheric ambient */
48
61
  body::before {
49
62
  content: '';
50
63
  position: fixed;
51
- top: -80px; left: -60px;
64
+ top: -80px; left: -50px;
52
65
  width: 260px; height: 260px;
53
- background: radial-gradient(circle, rgba(99,102,241,0.08) 0%, transparent 70%);
66
+ background: radial-gradient(circle, rgba(147, 129, 255, 0.07) 0%, transparent 65%);
54
67
  pointer-events: none;
55
68
  z-index: 0;
56
69
  }
57
70
  body::after {
58
71
  content: '';
59
72
  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%);
73
+ bottom: -60px; right: -50px;
74
+ width: 220px; height: 220px;
75
+ background: radial-gradient(circle, rgba(52, 211, 153, 0.05) 0%, transparent 65%);
63
76
  pointer-events: none;
64
77
  z-index: 0;
65
78
  }
@@ -70,339 +83,594 @@
70
83
  display: flex;
71
84
  flex-direction: column;
72
85
  height: 100%;
73
- padding: 10px;
74
- gap: 7px;
86
+ padding: 8px;
87
+ gap: 6px;
75
88
  }
76
89
 
77
- /* HEADER */
90
+ /* ── HEADER (Ghostty macOS Native Window Chrome) ── */
78
91
  .header {
79
92
  display: flex;
80
93
  align-items: center;
81
94
  justify-content: space-between;
82
- background: var(--surface-1);
83
- border: 1px solid var(--border-1);
95
+ background: var(--surface-base);
96
+ border: 1px solid var(--border-subtle);
84
97
  border-radius: var(--radius-md);
85
- padding: 8px 10px;
98
+ padding: 6px 8px;
99
+ flex-shrink: 0;
100
+ box-shadow: 0 1px 2px rgba(0,0,0,0.4);
101
+ }
102
+
103
+ .brand { display: flex; align-items: center; gap: 7px; min-width: 0; }
104
+
105
+ .brand-logo {
106
+ width: 22px; height: 22px;
107
+ border-radius: var(--radius-xs);
108
+ background: linear-gradient(135deg, #7c3aed 0%, #9381ff 60%, #34d399 100%);
109
+ display: flex; align-items: center; justify-content: center;
110
+ flex-shrink: 0;
111
+ box-shadow: 0 0 0 1px rgba(255,255,255,0.18) inset, 0 2px 8px rgba(124, 58, 237, 0.35);
112
+ color: #ffffff;
113
+ }
114
+
115
+ .brand-info { display: flex; flex-direction: column; min-width: 0; }
116
+ .brand-title-row { display: flex; align-items: baseline; gap: 5px; }
117
+ .brand-name { font-size: 11px; font-weight: 700; letter-spacing: -0.02em; color: var(--text-primary); line-height: 1; }
118
+ .brand-ver { font-size: 8px; font-weight: 600; color: var(--text-muted); font-family: var(--font-mono); }
119
+ .brand-file {
120
+ font-size: 9px;
121
+ color: var(--text-secondary);
122
+ font-weight: 500;
123
+ line-height: 1.2;
124
+ white-space: nowrap;
125
+ overflow: hidden;
126
+ text-overflow: ellipsis;
127
+ max-width: 140px;
128
+ margin-top: 1px;
129
+ }
130
+
131
+ .header-actions {
132
+ display: flex;
133
+ align-items: center;
134
+ gap: 4px;
86
135
  flex-shrink: 0;
136
+ }
137
+
138
+ .header-icon-btn {
139
+ width: 22px;
140
+ height: 22px;
141
+ padding: 0;
142
+ border-radius: var(--radius-xs);
143
+ background: var(--surface-card);
144
+ border: 1px solid var(--border-subtle);
145
+ color: var(--text-secondary);
146
+ display: flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ cursor: pointer;
150
+ transition: all var(--transition);
151
+ }
152
+ .header-icon-btn:hover {
153
+ background: var(--surface-hover);
154
+ border-color: var(--border-light);
155
+ color: var(--text-primary);
156
+ }
157
+ .header-icon-btn:active { transform: scale(0.94); }
158
+ .header-icon-btn.indexing svg {
159
+ animation: spin 1s linear infinite;
160
+ color: var(--ghost-lavender);
161
+ }
162
+
163
+ .status-pill {
164
+ display: flex; align-items: center; gap: 5px;
165
+ background: var(--surface-card);
166
+ border: 1px solid var(--border-subtle);
167
+ border-radius: var(--radius-full);
168
+ padding: 2.5px 7px 2.5px 5px;
169
+ font-size: 9px; font-weight: 600;
170
+ color: var(--text-secondary);
171
+ transition: all var(--transition);
172
+ cursor: pointer;
173
+ font-family: var(--font-mono);
174
+ }
175
+ .status-pill:hover { border-color: var(--border-light); background: var(--surface-hover); color: var(--text-primary); }
176
+ .status-pill.connected { border-color: rgba(52,211,153,0.25); background: rgba(52,211,153,0.06); color: var(--ghost-mint); }
177
+ .status-pill.offline { border-color: rgba(251,113,133,0.25); background: rgba(251,113,133,0.06); color: var(--ghost-rose); }
178
+ .status-pill.connecting { border-color: rgba(251,191,36,0.25); background: rgba(251,191,36,0.06); color: var(--ghost-amber); }
179
+
180
+ .dot {
181
+ width: 6px; height: 6px;
182
+ border-radius: 50%; flex-shrink: 0;
183
+ transition: all var(--transition);
184
+ }
185
+ .dot.green { background: var(--ghost-mint); box-shadow: 0 0 0 2px rgba(52,211,153,0.2), 0 0 8px rgba(52,211,153,0.6); }
186
+ .dot.yellow { background: var(--ghost-amber); box-shadow: 0 0 0 2px rgba(251,191,36,0.2), 0 0 8px rgba(251,191,36,0.5); animation: pulse 1.4s ease-in-out infinite; }
187
+ .dot.red { background: var(--ghost-rose); box-shadow: 0 0 0 2px rgba(251,113,133,0.2), 0 0 8px rgba(251,113,133,0.5); }
188
+
189
+ @keyframes pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.3; transform: scale(0.85); } }
190
+
191
+ /* ── SELECTION INSPECTOR (Ghostty Split-Pane Card) ── */
192
+ .selection-card {
193
+ display: flex;
194
+ align-items: center;
195
+ justify-content: space-between;
196
+ gap: 7px;
197
+ background: var(--surface-card);
198
+ border: 1px solid var(--border-subtle);
199
+ border-radius: var(--radius-sm);
200
+ padding: 6px 8px;
201
+ flex-shrink: 0;
202
+ transition: border-color var(--transition), background var(--transition), box-shadow var(--transition);
87
203
  position: relative;
88
204
  overflow: hidden;
89
205
  }
90
- .header::before {
206
+ .selection-card::before {
91
207
  content: '';
92
208
  position: absolute;
93
- top: 0; left: 0; right: 0;
94
- height: 1px;
209
+ top: 0; left: 0; right: 0; height: 1px;
95
210
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent);
96
211
  }
212
+ .selection-card.has-selection {
213
+ border-color: var(--border-ghost);
214
+ background: linear-gradient(135deg, rgba(147, 129, 255, 0.05) 0%, rgba(52, 211, 153, 0.02) 100%), var(--surface-card);
215
+ }
216
+ .selection-card.has-selection:hover {
217
+ border-color: rgba(147, 129, 255, 0.55);
218
+ background: linear-gradient(135deg, rgba(147, 129, 255, 0.09) 0%, rgba(52, 211, 153, 0.04) 100%), var(--surface-card);
219
+ }
97
220
 
98
- .brand { display: flex; align-items: center; gap: 9px; }
221
+ .selection-main {
222
+ display: flex;
223
+ align-items: center;
224
+ gap: 7px;
225
+ min-width: 0;
226
+ flex: 1;
227
+ cursor: pointer;
228
+ }
99
229
 
100
- .brand-logo {
101
- width: 26px; height: 26px;
102
- border-radius: 7px;
103
- background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%);
230
+ .selection-icon-wrap {
231
+ width: 22px; height: 22px;
232
+ border-radius: var(--radius-xs);
233
+ background: var(--surface-base);
234
+ border: 1px solid var(--border-subtle);
104
235
  display: flex; align-items: center; justify-content: center;
105
236
  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);
237
+ color: var(--text-muted);
238
+ transition: all var(--transition);
239
+ }
240
+ .selection-card.has-selection .selection-icon-wrap {
241
+ background: rgba(147, 129, 255, 0.12);
242
+ border-color: rgba(147, 129, 255, 0.35);
243
+ color: var(--ghost-lavender);
107
244
  }
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
245
 
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);
246
+ .selection-text { display: flex; flex-direction: column; min-width: 0; }
247
+ .selection-name {
248
+ font-size: 11px;
249
+ font-weight: 600;
250
+ color: var(--text-primary);
251
+ line-height: 1.2;
252
+ white-space: nowrap;
253
+ overflow: hidden;
254
+ text-overflow: ellipsis;
255
+ }
256
+ .selection-meta {
257
+ font-size: 9px;
258
+ color: var(--text-secondary);
259
+ font-family: var(--font-mono);
260
+ line-height: 1.2;
261
+ white-space: nowrap;
262
+ overflow: hidden;
263
+ text-overflow: ellipsis;
122
264
  }
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
265
 
126
- .dot {
127
- width: 7px; height: 7px;
128
- border-radius: 50%; flex-shrink: 0;
129
- transition: background var(--transition), box-shadow var(--transition);
266
+ .selection-actions {
267
+ display: flex;
268
+ align-items: center;
269
+ gap: 4px;
270
+ flex-shrink: 0;
130
271
  }
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
272
 
135
- @keyframes blink {
136
- 0%, 100% { opacity: 1; }
137
- 50% { opacity: 0.2; }
273
+ .act-btn {
274
+ display: inline-flex;
275
+ align-items: center;
276
+ justify-content: center;
277
+ gap: 4px;
278
+ height: 22px;
279
+ padding: 0 6px;
280
+ border-radius: var(--radius-xs);
281
+ font-size: 8.5px;
282
+ font-weight: 600;
283
+ font-family: var(--font-mono);
284
+ background: var(--surface-base);
285
+ border: 1px solid var(--border-subtle);
286
+ color: var(--text-secondary);
287
+ cursor: pointer;
288
+ transition: all var(--transition);
289
+ letter-spacing: 0.2px;
290
+ }
291
+ .act-btn:hover {
292
+ background: var(--surface-hover);
293
+ border-color: var(--border-light);
294
+ color: var(--text-primary);
295
+ }
296
+ .act-btn:active { transform: scale(0.95); }
297
+ .act-btn.icon-only { width: 22px; padding: 0; }
298
+ .act-btn.badge-btn {
299
+ background: rgba(147, 129, 255, 0.12);
300
+ border-color: rgba(147, 129, 255, 0.28);
301
+ color: #d1c9ff;
302
+ }
303
+ .act-btn.badge-btn:hover {
304
+ background: rgba(147, 129, 255, 0.28);
305
+ border-color: rgba(147, 129, 255, 0.6);
306
+ color: #ffffff;
138
307
  }
139
308
 
140
- /* STATS ROW */
141
- .stats-row {
309
+ /* ── TELEMETRY SEGMENTED STATUSLINE ── */
310
+ .telemetry-ribbon {
142
311
  display: grid;
143
312
  grid-template-columns: repeat(4, 1fr);
144
- gap: 5px;
145
- flex-shrink: 0;
146
- }
147
- .stats-subrow {
148
- display: grid;
149
- grid-template-columns: 1fr 1fr auto;
150
- gap: 5px;
313
+ background: var(--surface-base);
314
+ border: 1px solid var(--border-subtle);
315
+ border-radius: var(--radius-sm);
316
+ overflow: hidden;
151
317
  flex-shrink: 0;
152
318
  }
153
319
 
154
- .stat-card {
155
- background: var(--surface-1);
156
- border: 1px solid var(--border-1);
157
- border-radius: var(--radius-sm);
158
- padding: 6px 7px;
159
- display: flex; flex-direction: column; gap: 2px;
160
- transition: border-color var(--transition);
161
- }
162
- .stat-card:hover { border-color: var(--border-2); }
163
-
164
- .stat-label {
165
- font-size: 8px; font-weight: 700;
166
- letter-spacing: 0.7px; text-transform: uppercase;
167
- color: var(--text-3);
168
- }
169
- .stat-value {
170
- font-size: 15px; font-weight: 800;
171
- font-family: 'SF Mono', 'Fira Code', monospace;
172
- line-height: 1.1;
173
- display: flex; align-items: baseline; gap: 2px;
174
- }
175
- .stat-unit {
176
- font-size: 8.5px; font-weight: 600; color: var(--text-3);
177
- }
178
- .stat-card.writes .stat-value { color: var(--violet); }
179
- .stat-card.reads .stat-value { color: var(--accent-2); }
180
- .stat-card.errors .stat-value { color: var(--rose); }
181
- .stat-card.speed .stat-value { color: var(--green); }
182
- .stat-card.memory .stat-value { color: var(--amber); }
183
-
184
- .port-card {
185
- background: var(--surface-1);
186
- border: 1px solid var(--border-1);
187
- border-radius: var(--radius-sm);
188
- padding: 6px 9px;
189
- display: flex; flex-direction: column; gap: 2px;
190
- cursor: pointer;
320
+ .telemetry-cell {
321
+ display: flex;
322
+ flex-direction: column;
323
+ align-items: center;
191
324
  justify-content: center;
192
- transition: border-color var(--transition), background var(--transition);
325
+ padding: 4px 2px;
326
+ gap: 1px;
327
+ position: relative;
328
+ transition: background var(--transition);
193
329
  }
194
- .port-card:hover { border-color: rgba(6,182,212,0.35); background: rgba(6,182,212,0.04); }
195
- .port-label { font-size: 8px; font-weight: 700; letter-spacing: 0.7px; text-transform: uppercase; color: var(--text-3); }
196
- .port-value { font-size: 12px; font-weight: 700; font-family: 'SF Mono', 'Fira Code', monospace; color: var(--accent-2); line-height: 1.1; }
330
+ .telemetry-cell:not(:last-child)::after {
331
+ content: '';
332
+ position: absolute;
333
+ right: 0; top: 15%; bottom: 15%;
334
+ width: 1px;
335
+ background: var(--border-subtle);
336
+ }
337
+ .telemetry-cell:hover { background: rgba(255, 255, 255, 0.02); }
338
+
339
+ .telemetry-label {
340
+ font-size: 7.5px;
341
+ font-weight: 700;
342
+ letter-spacing: 0.6px;
343
+ text-transform: uppercase;
344
+ color: var(--text-muted);
345
+ font-family: var(--font-mono);
346
+ }
347
+ .telemetry-value {
348
+ font-size: 13px;
349
+ font-weight: 700;
350
+ font-family: var(--font-mono);
351
+ line-height: 1;
352
+ font-variant-numeric: tabular-nums;
353
+ display: flex;
354
+ align-items: baseline;
355
+ gap: 1.5px;
356
+ }
357
+ .telemetry-unit { font-size: 7.5px; font-weight: 600; color: var(--text-muted); }
197
358
 
198
- /* TERMINAL */
359
+ .telemetry-cell.writes .telemetry-value { color: var(--ghost-lavender); }
360
+ .telemetry-cell.reads .telemetry-value { color: var(--ghost-cyan); }
361
+ .telemetry-cell.speed .telemetry-value { color: var(--ghost-mint); }
362
+ .telemetry-cell.ram .telemetry-value { color: var(--ghost-peach); }
363
+
364
+ /* ── GHOSTTY STREAM CONSOLE BUFFER ── */
199
365
  .terminal-wrap {
200
366
  flex: 1; min-height: 0;
201
367
  display: flex; flex-direction: column;
202
- background: var(--surface-1);
203
- border: 1px solid var(--border-1);
368
+ background: var(--surface-base);
369
+ border: 1px solid var(--border-subtle);
204
370
  border-radius: var(--radius-md);
205
371
  overflow: hidden;
372
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3);
206
373
  }
207
374
 
208
375
  .terminal-header {
209
376
  display: flex; align-items: center; justify-content: space-between;
210
- padding: 6px 10px;
211
- background: var(--surface-2);
212
- border-bottom: 1px solid var(--border-1);
377
+ padding: 5px 7px;
378
+ background: rgba(255,255,255,0.02);
379
+ border-bottom: 1px solid var(--border-subtle);
213
380
  flex-shrink: 0;
214
381
  }
215
382
 
216
- .terminal-title {
217
- display: flex; align-items: center; gap: 6px;
218
- font-size: 9px; font-weight: 700;
219
- letter-spacing: 0.8px; text-transform: uppercase;
220
- color: var(--text-3);
383
+ .terminal-tabs { display: flex; align-items: center; gap: 3px; }
384
+
385
+ .t-tab {
386
+ font-size: 8.5px;
387
+ font-weight: 600;
388
+ font-family: var(--font-mono);
389
+ letter-spacing: 0.2px;
390
+ padding: 2px 7px;
391
+ border-radius: var(--radius-xs);
392
+ color: var(--text-muted);
393
+ background: transparent;
394
+ border: none;
395
+ cursor: pointer;
396
+ transition: all var(--transition);
221
397
  }
222
- .t-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); opacity: 0.5; }
398
+ .t-tab:hover { color: var(--text-secondary); background: rgba(255,255,255,0.04); }
399
+ .t-tab.active { color: var(--text-primary); background: var(--surface-card); box-shadow: 0 1px 2px rgba(0,0,0,0.3); }
400
+
401
+ .terminal-meta { display: flex; align-items: center; gap: 6px; }
223
402
 
224
403
  .event-badge {
225
- font-size: 9px; font-weight: 600;
226
- color: var(--text-3);
227
- background: var(--surface-3);
228
- border: 1px solid var(--border-1);
229
- border-radius: 100px;
230
- padding: 1px 7px;
404
+ font-size: 8px;
405
+ font-weight: 600;
406
+ font-family: var(--font-mono);
407
+ color: var(--text-muted);
408
+ background: var(--surface-card);
409
+ border: 1px solid var(--border-subtle);
410
+ border-radius: var(--radius-full);
411
+ padding: 1px 6px;
231
412
  }
232
413
 
233
414
  .terminal-body {
234
415
  flex: 1;
235
- padding: 8px 10px;
236
- font-family: 'SF Mono', 'JetBrains Mono', 'Fira Code', monospace;
237
- font-size: 10.5px; line-height: 1.6;
416
+ padding: 6px 8px;
417
+ font-family: var(--font-mono);
418
+ font-size: 9.5px;
419
+ line-height: 1.5;
238
420
  overflow-y: auto;
239
- color: var(--text-2);
421
+ color: var(--text-secondary);
240
422
  }
241
423
  .terminal-body::-webkit-scrollbar { width: 3px; }
242
424
  .terminal-body::-webkit-scrollbar-track { background: transparent; }
243
- .terminal-body::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 2px; }
425
+ .terminal-body::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 2px; }
244
426
 
245
427
  .log-line {
246
- display: flex; align-items: baseline; gap: 8px;
247
- padding: 1px 3px; border-radius: 3px;
428
+ display: flex;
429
+ align-items: baseline;
430
+ gap: 6px;
431
+ padding: 1.5px 3px;
432
+ border-radius: 3px;
433
+ transition: background 0.1s ease;
248
434
  }
249
- .log-line:hover { background: rgba(255,255,255,0.02); }
250
- .log-ts { font-size: 9px; color: var(--text-3); flex-shrink: 0; letter-spacing: -0.2px; }
251
- .log-msg { flex: 1; word-break: break-all; }
252
- .log-line .ok { color: var(--green); }
253
- .log-line .err { color: var(--rose); }
254
- .log-line .info { color: var(--accent-2); }
435
+ .log-line:hover { background: rgba(255,255,255,0.03); }
436
+ .log-ts { font-size: 8px; color: var(--text-muted); flex-shrink: 0; letter-spacing: -0.2px; }
437
+ .log-prompt { color: var(--ghost-lavender); font-size: 9px; flex-shrink: 0; font-weight: 700; }
438
+ .log-msg { flex: 1; word-break: break-all; color: var(--text-secondary); }
439
+ .log-msg.ok { color: var(--ghost-mint); }
440
+ .log-msg.err { color: var(--ghost-rose); }
441
+ .log-msg.info { color: var(--ghost-cyan); }
255
442
 
256
443
  .terminal-empty {
257
444
  display: flex; flex-direction: column;
258
445
  align-items: center; justify-content: center;
259
- height: 100%; gap: 6px; color: var(--text-3);
446
+ height: 100%; gap: 5px; color: var(--text-muted);
260
447
  }
261
- .terminal-empty-icon { font-size: 20px; opacity: 0.4; }
262
- .terminal-empty-text { font-size: 10px; opacity: 0.5; }
448
+ .terminal-empty-icon { font-size: 16px; opacity: 0.35; }
449
+ .terminal-empty-text { font-size: 9.5px; opacity: 0.6; font-family: var(--font-mono); }
263
450
 
264
- /* FOOTER */
451
+ /* ── FOOTER ACTIONS ── */
265
452
  .footer { display: flex; gap: 6px; flex-shrink: 0; }
266
453
 
267
454
  .btn {
268
455
  display: inline-flex; align-items: center; justify-content: center;
269
- gap: 5px; padding: 7px 11px;
456
+ gap: 6px; padding: 6px 11px;
270
457
  border-radius: var(--radius-sm);
271
- font-size: 10.5px; font-weight: 600;
458
+ font-size: 10px; font-weight: 600;
459
+ font-family: var(--font-ui);
272
460
  cursor: pointer;
273
- border: 1px solid var(--border-2);
274
- background: var(--surface-2);
275
- color: var(--text-2);
461
+ border: 1px solid var(--border-subtle);
462
+ background: var(--surface-card);
463
+ color: var(--text-secondary);
276
464
  transition: all var(--transition);
277
465
  letter-spacing: 0.1px;
278
466
  white-space: nowrap;
279
467
  }
280
- .btn:hover { background: var(--surface-3); border-color: var(--border-3); color: var(--text-1); }
281
- .btn:active { transform: scale(0.96); }
468
+ .btn:hover { background: var(--surface-hover); border-color: var(--border-light); color: var(--text-primary); }
469
+ .btn:active { transform: scale(0.97); }
282
470
 
283
471
  .btn-icon { width: 30px; height: 30px; padding: 0; flex-shrink: 0; }
284
472
 
285
473
  .btn-primary {
286
474
  flex: 1;
287
- background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%);
288
- border-color: transparent; color: #fff; font-weight: 700;
289
- box-shadow: 0 1px 0 rgba(255,255,255,0.1) inset, 0 4px 16px rgba(79,70,229,0.3);
475
+ background: linear-gradient(135deg, rgba(124,58,237,0.9) 0%, rgba(147,129,255,0.9) 50%, rgba(52,211,153,0.85) 100%);
476
+ border-color: rgba(147, 129, 255, 0.45);
477
+ color: #ffffff;
478
+ font-weight: 600;
479
+ box-shadow: 0 1px 0 rgba(255,255,255,0.2) inset, 0 3px 12px rgba(124, 58, 237, 0.28);
290
480
  }
291
481
  .btn-primary:hover {
292
- opacity: 0.9; border-color: transparent; color: #fff;
293
- box-shadow: 0 1px 0 rgba(255,255,255,0.12) inset, 0 6px 20px rgba(79,70,229,0.4);
482
+ background: linear-gradient(135deg, #7c3aed 0%, #9381ff 50%, #34d399 100%);
483
+ border-color: rgba(147, 129, 255, 0.7);
484
+ color: #ffffff;
485
+ box-shadow: 0 1px 0 rgba(255,255,255,0.3) inset, 0 4px 16px rgba(124, 58, 237, 0.45);
294
486
  }
295
487
 
296
- .btn-live {
297
- flex: 1;
298
- background: rgba(34,211,160,0.08);
299
- border-color: rgba(34,211,160,0.25);
300
- color: var(--green);
488
+ .btn-primary.indexing svg { animation: spin 1s linear infinite; }
489
+ @keyframes spin { 100% { transform: rotate(360deg); } }
490
+
491
+ /* ── TOAST NOTIFICATION ── */
492
+ .toast {
493
+ position: fixed;
494
+ bottom: 45px;
495
+ left: 50%;
496
+ transform: translateX(-50%) translateY(15px);
497
+ opacity: 0;
498
+ pointer-events: none;
499
+ background: rgba(18, 20, 26, 0.96);
500
+ border: 1px solid rgba(147, 129, 255, 0.3);
501
+ backdrop-filter: blur(8px);
502
+ box-shadow: 0 8px 24px rgba(0,0,0,0.6), 0 0 12px rgba(147, 129, 255, 0.2);
503
+ border-radius: var(--radius-full);
504
+ padding: 5px 12px;
505
+ font-size: 9.5px;
506
+ font-weight: 600;
507
+ color: var(--text-primary);
508
+ display: flex;
509
+ align-items: center;
510
+ gap: 6px;
511
+ z-index: 100;
512
+ transition: all 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
513
+ }
514
+ .toast.show {
515
+ opacity: 1;
516
+ transform: translateX(-50%) translateY(0);
517
+ pointer-events: auto;
301
518
  }
302
- .btn-live:hover { background: rgba(34,211,160,0.12); border-color: rgba(34,211,160,0.4); color: var(--green); }
303
519
 
304
520
  /* RESIZE GRIP */
305
521
  .resize-grip {
306
522
  position: fixed; right: 0; bottom: 0;
307
- width: 24px; height: 24px;
523
+ width: 18px; height: 18px;
308
524
  cursor: nwse-resize; opacity: 0.35;
309
- color: var(--text-2);
525
+ color: var(--text-secondary);
310
526
  transition: opacity var(--transition), color var(--transition);
311
527
  z-index: 1000;
312
528
  display: flex; align-items: flex-end; justify-content: flex-end;
313
- padding: 4px;
529
+ padding: 3px;
314
530
  touch-action: none;
315
531
  }
316
- .resize-grip:hover { opacity: 1; color: var(--accent-2); }
532
+ .resize-grip:hover { opacity: 1; color: var(--ghost-mint); }
317
533
  </style>
318
534
  </head>
319
535
  <body>
320
536
 
321
537
  <div class="app">
322
538
 
539
+ <!-- ── HEADER (Ghostty macOS Native Window Chrome) ── -->
323
540
  <header class="header">
324
541
  <div class="brand">
325
542
  <div class="brand-logo">
326
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
327
- <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
543
+ <!-- Ghostty Ghost Mascot Icon -->
544
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
545
+ <path d="M9 10h.01M15 10h.01M12 2a8 8 0 0 0-8 8v11l3-2 3 2 2-2 2 2 3-2 3 2V10a8 8 0 0 0-8-8z"/>
328
546
  </svg>
329
547
  </div>
330
548
  <div class="brand-info">
331
- <span class="brand-name">Figma MCP</span>
332
- <span class="brand-file" id="session-display">READY</span>
549
+ <div class="brand-title-row">
550
+ <span class="brand-name">Figma MCP</span>
551
+ <span class="brand-ver">v2.8</span>
552
+ </div>
553
+ <span class="brand-file" id="doc-display" title="Active Document">Ready</span>
333
554
  </div>
334
555
  </div>
335
556
 
336
- <div class="status-pill" id="status-pill">
337
- <div class="dot yellow" id="dot"></div>
338
- <span id="status-text">Connecting</span>
339
- <span id="mode-badge" style="display:none;color:var(--text-3)">—</span>
557
+ <div class="header-actions">
558
+ <!-- Re-index Icon Button -->
559
+ <button class="header-icon-btn" id="reindex-btn" onclick="triggerManualReindex()" title="Sync Index (Re-index Figma canvas to MCP)">
560
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
561
+ <polygon points="12 2 2 7 12 12 22 7 12 2"/>
562
+ <polyline points="2 17 12 22 22 17"/>
563
+ <polyline points="2 12 12 17 22 12"/>
564
+ </svg>
565
+ </button>
566
+
567
+ <!-- Reconnect / Probe Bridge Port Button -->
568
+ <button class="header-icon-btn" id="reconnect-btn" onclick="reconnect()" title="Reconnect / Probe Bridge Port">
569
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
570
+ <polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
571
+ </svg>
572
+ </button>
573
+
574
+ <!-- Connection Status Pill -->
575
+ <div class="status-pill" id="status-pill" onclick="handleStatusClick()" title="Connection Status (Click for details)">
576
+ <div class="dot yellow" id="dot"></div>
577
+ <span id="status-text">Connecting</span>
578
+ </div>
340
579
  </div>
341
580
  </header>
342
581
 
343
- <div class="stats-row">
344
- <div class="stat-card writes" title="Write Operations">
345
- <span class="stat-label">Writes</span>
346
- <span class="stat-value" id="write-count">0</span>
582
+ <!-- ── SELECTION INSPECTOR HERO CARD ── -->
583
+ <div class="selection-card" id="selection-card">
584
+ <div class="selection-main" onclick="focusSelection()" title="Click to center view on selection">
585
+ <div class="selection-icon-wrap" id="selection-icon">
586
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
587
+ <circle cx="12" cy="12" r="9"/>
588
+ </svg>
589
+ </div>
590
+ <div class="selection-text">
591
+ <span class="selection-name" id="selection-name">No selection</span>
592
+ <span class="selection-meta" id="selection-meta">Document canvas root</span>
593
+ </div>
594
+ </div>
595
+
596
+ <div class="selection-actions" id="selection-actions" style="display:none;">
597
+ <!-- Snapshot PNG button -->
598
+ <button class="act-btn icon-only" id="snapshot-btn" onclick="exportSelectionImage(event)" title="📸 Copy layer PNG (2x) to clipboard">
599
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
600
+ <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
601
+ <circle cx="12" cy="13" r="4"/>
602
+ </svg>
603
+ </button>
604
+
605
+ <!-- Focus / Center on Canvas button -->
606
+ <button class="act-btn icon-only" onclick="focusSelection()" title="🎯 Center view on canvas">
607
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
608
+ <circle cx="12" cy="12" r="10"/>
609
+ <line x1="22" y1="12" x2="18" y2="12"/>
610
+ <line x1="6" y1="12" x2="2" y2="12"/>
611
+ <line x1="12" y1="6" x2="12" y2="2"/>
612
+ <line x1="12" y1="22" x2="12" y2="18"/>
613
+ </svg>
614
+ </button>
615
+
616
+ <!-- Copy Node ID badge -->
617
+ <button class="act-btn badge-btn" id="selection-badge" onclick="copySelectionId(event)" title="📋 Click to copy Figma Node ID">
618
+ FRAME
619
+ </button>
620
+ </div>
621
+ </div>
622
+
623
+ <!-- ── TELEMETRY SEGMENTED RIBBON ── -->
624
+ <div class="telemetry-ribbon">
625
+ <div class="telemetry-cell writes" title="Write operations executed by AI">
626
+ <span class="telemetry-label">Writes</span>
627
+ <span class="telemetry-value" id="write-count">0</span>
347
628
  </div>
348
- <div class="stat-card reads" title="Read Operations">
349
- <span class="stat-label">Reads</span>
350
- <span class="stat-value" id="read-count">0</span>
629
+ <div class="telemetry-cell reads" title="Read / Inspection operations executed">
630
+ <span class="telemetry-label">Reads</span>
631
+ <span class="telemetry-value" id="read-count">0</span>
351
632
  </div>
352
- <div class="stat-card speed" title="Last Request Execution Time / Speed">
353
- <span class="stat-label">Speed</span>
354
- <span class="stat-value"><span id="speed-display">0</span><span class="stat-unit">ms</span></span>
633
+ <div class="telemetry-cell speed" title="Execution latency of last request">
634
+ <span class="telemetry-label">Latency</span>
635
+ <span class="telemetry-value"><span id="speed-display">0</span><span class="telemetry-unit">ms</span></span>
355
636
  </div>
356
- <div class="stat-card memory" title="Server Memory Usage (RAM RSS)">
357
- <span class="stat-label">RAM</span>
358
- <span class="stat-value"><span id="ram-display">3.2</span><span class="stat-unit">MB</span></span>
637
+ <div class="telemetry-cell ram" title="Rust MCP Server Memory (RAM RSS)">
638
+ <span class="telemetry-label">RAM</span>
639
+ <span class="telemetry-value"><span id="ram-display">3.2</span><span class="telemetry-unit">MB</span></span>
359
640
  </div>
360
641
  </div>
361
642
 
643
+ <!-- ── STREAM CONSOLE TERMINAL ── -->
362
644
  <section class="terminal-wrap">
363
645
  <div class="terminal-header">
364
- <div class="terminal-title">
365
- <div class="t-dot"></div>
366
- Stream Console
646
+ <div class="terminal-tabs">
647
+ <button class="t-tab active" onclick="setLogFilter('all', this)">All</button>
648
+ <button class="t-tab" onclick="setLogFilter('writes', this)">Writes</button>
649
+ <button class="t-tab" onclick="setLogFilter('reads', this)">Reads</button>
650
+ </div>
651
+ <div class="terminal-meta">
652
+ <span class="event-badge" id="log-count">0 ev</span>
653
+ <button class="act-btn icon-only" onclick="clearLog()" title="Clear stream console" style="height:18px;width:18px;border-radius:3px;">
654
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
655
+ <polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/>
656
+ </svg>
657
+ </button>
367
658
  </div>
368
- <span class="event-badge" id="log-count">0 events</span>
369
659
  </div>
370
660
  <div class="terminal-body" id="log">
371
661
  <div class="terminal-empty" id="log-empty">
372
662
  <div class="terminal-empty-icon">◌</div>
373
- <div class="terminal-empty-text">Waiting for events…</div>
663
+ <div class="terminal-empty-text">Listening on MCP bridge…</div>
374
664
  </div>
375
665
  </div>
376
666
  </section>
377
667
 
378
- <footer class="footer">
379
- <button class="btn btn-icon" onclick="clearLog()" title="Clear log">
380
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
381
- <polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/>
382
- </svg>
383
- </button>
384
-
385
- <button class="btn" id="reindex-btn" onclick="triggerManualReindex()" title="Re-index Figma document to Rust memory" style="background:var(--surface-2);border:1px solid var(--border-2);color:var(--text-2);font-size:10px;padding:0 10px;gap:5px;">
386
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
387
- <path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"/>
388
- </svg>
389
- <span id="reindex-btn-label">⚡ Index</span>
390
- </button>
391
-
392
- <button class="btn btn-primary" id="launch-btn" onclick="launchServer()" title="Start Figma MCP server">
393
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
394
- <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
395
- </svg>
396
- <span id="launch-btn-label">Launch Server</span>
397
- </button>
398
-
399
- <button class="btn btn-icon" onclick="reconnect()" title="Reconnect">
400
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
401
- <polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
402
- </svg>
403
- </button>
404
- </footer>
668
+ </div>
405
669
 
670
+ <!-- Floating Toast Component -->
671
+ <div class="toast" id="toast">
672
+ <span id="toast-icon">✓</span>
673
+ <span id="toast-text">Action completed</span>
406
674
  </div>
407
675
 
408
676
  <div class="resize-grip" id="resize-grip" title="Drag to resize">
@@ -427,21 +695,62 @@
427
695
  var writeCount = 0, readCount = 0, errCount = 0;
428
696
  var totalLogEvents = 0;
429
697
  var polling = true;
698
+ var activeLogFilter = 'all';
699
+
700
+ var logEl = document.getElementById("log");
701
+ var logEmptyEl = document.getElementById("log-empty");
702
+ var dotEl = document.getElementById("dot");
703
+ var statusEl = document.getElementById("status-text");
704
+ var statusPill = document.getElementById("status-pill");
705
+ var logCountEl = document.getElementById("log-count");
706
+ var docDispEl = document.getElementById("doc-display");
707
+ var speedEl = document.getElementById("speed-display");
708
+ var ramEl = document.getElementById("ram-display");
709
+ var reindexBtn = document.getElementById("reindex-btn");
710
+ var reindexBtnLabel = document.getElementById("reindex-btn-label");
711
+
712
+ var selCardEl = document.getElementById("selection-card");
713
+ var selIconEl = document.getElementById("selection-icon");
714
+ var selNameEl = document.getElementById("selection-name");
715
+ var selMetaEl = document.getElementById("selection-meta");
716
+ var selActionsEl = document.getElementById("selection-actions");
717
+ var selBadgeEl = document.getElementById("selection-badge");
718
+ var snapshotBtn = document.getElementById("snapshot-btn");
719
+ var toastEl = document.getElementById("toast");
720
+ var toastTextEl = document.getElementById("toast-text");
721
+ var toastIconEl = document.getElementById("toast-icon");
722
+
723
+ var currentSelectionCount = 0;
724
+ var currentSelectionId = null;
725
+ var currentSelectionName = "";
726
+
727
+ function showToast(msg, icon) {
728
+ if (!toastEl) return;
729
+ if (toastTextEl) toastTextEl.textContent = msg;
730
+ if (toastIconEl) toastIconEl.textContent = icon || "✓";
731
+ toastEl.classList.add("show");
732
+ setTimeout(function() {
733
+ toastEl.classList.remove("show");
734
+ }, 2200);
735
+ }
430
736
 
431
- var logEl = document.getElementById("log");
432
- var logEmptyEl = document.getElementById("log-empty");
433
- var dotEl = document.getElementById("dot");
434
- var statusEl = document.getElementById("status-text");
435
- var modeEl = document.getElementById("mode-badge");
436
- var logCountEl = document.getElementById("log-count");
437
- var sessionDispEl = document.getElementById("session-display");
438
- var launchBtn = document.getElementById("launch-btn");
439
- var launchBtnLabel = document.getElementById("launch-btn-label");
440
- var statusPill = document.getElementById("status-pill");
441
- var speedEl = document.getElementById("speed-display");
442
- var ramEl = document.getElementById("ram-display");
443
- var reindexBtn = document.getElementById("reindex-btn");
444
- var reindexBtnLabel = document.getElementById("reindex-btn-label");
737
+ function setLogFilter(filter, el) {
738
+ activeLogFilter = filter;
739
+ var tabs = document.querySelectorAll(".t-tab");
740
+ tabs.forEach(function(t) { t.classList.remove("active"); });
741
+ if (el) el.classList.add("active");
742
+
743
+ var lines = logEl.querySelectorAll(".log-line");
744
+ lines.forEach(function(line) {
745
+ if (filter === 'all') {
746
+ line.style.display = "flex";
747
+ } else if (filter === 'writes') {
748
+ line.style.display = line.dataset.type === 'write' ? "flex" : "none";
749
+ } else if (filter === 'reads') {
750
+ line.style.display = line.dataset.type === 'read' ? "flex" : "none";
751
+ }
752
+ });
753
+ }
445
754
 
446
755
  async function fetchServerStats() {
447
756
  if (!everConnected) return;
@@ -460,13 +769,42 @@
460
769
  function triggerManualReindex() {
461
770
  if (!everConnected) {
462
771
  log("Cannot index: Server is offline", "err");
772
+ showSetupGuide();
463
773
  return;
464
774
  }
465
- if (reindexBtnLabel) reindexBtnLabel.textContent = "Indexing…";
775
+ if (reindexBtn) reindexBtn.classList.add("indexing");
776
+ showToast("Syncing canvas index…", "⚡");
466
777
  log("⚡ Manual re-index triggered…", "info");
467
778
  parent.postMessage({ pluginMessage: { type: "manual-reindex" } }, "*");
468
779
  }
469
780
 
781
+ function focusSelection() {
782
+ if (currentSelectionCount > 0) {
783
+ parent.postMessage({ pluginMessage: { type: "zoom-to-selection" } }, "*");
784
+ }
785
+ }
786
+
787
+ function exportSelectionImage(event) {
788
+ if (event) event.stopPropagation();
789
+ if (currentSelectionCount === 0) return;
790
+ if (snapshotBtn) snapshotBtn.style.opacity = "0.5";
791
+ parent.postMessage({ pluginMessage: { type: "export-selection-image", scale: 2 } }, "*");
792
+ }
793
+
794
+ function copySelectionId(event) {
795
+ if (event) event.stopPropagation();
796
+ if (!currentSelectionId) return;
797
+ if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
798
+ navigator.clipboard.writeText(currentSelectionId).then(function() {
799
+ showToast("Node ID: " + currentSelectionId + " copied", "📋");
800
+ }).catch(function() {
801
+ prompt("Copy Node ID:", currentSelectionId);
802
+ });
803
+ } else {
804
+ prompt("Copy Node ID:", currentSelectionId);
805
+ }
806
+ }
807
+
470
808
  var READ_OPS = [
471
809
  "get_selection","get_design","get_page_nodes","screenshot",
472
810
  "get_styles","get_local_components","get_viewport","get_variables",
@@ -474,33 +812,22 @@
474
812
  "index_scan"
475
813
  ];
476
814
 
477
- function setStatus(state, text, mode) {
478
- dotEl.className = "dot " + state;
815
+ function setStatus(state, text) {
816
+ dotEl.className = "dot " + (state === "green" ? "green" : (state === "yellow" ? "yellow" : "red"));
479
817
  statusEl.textContent = text;
480
- if (mode && modeEl) modeEl.textContent = mode;
481
818
 
482
819
  statusPill.className = "status-pill";
483
- if (state === "green") statusPill.classList.add("connected");
484
- if (state === "red") statusPill.classList.add("offline");
485
-
486
- if (launchBtn && launchBtnLabel) {
487
- launchBtn.className = "btn";
488
- if (state === "green") {
489
- launchBtn.classList.add("btn-live");
490
- launchBtnLabel.textContent = "● Live";
491
- } else {
492
- launchBtn.classList.add("btn-primary");
493
- launchBtnLabel.textContent = state === "yellow" ? "Connecting…" : "Launch Server";
494
- }
495
- }
820
+ if (state === "green") statusPill.classList.add("connected");
821
+ if (state === "red") statusPill.classList.add("offline");
822
+ if (state === "yellow") statusPill.classList.add("connecting");
496
823
  }
497
824
 
498
- function launchServer() {
499
- if (wsConnected) {
500
- log("Server is already running and connected!", "ok");
501
- return;
825
+ function handleStatusClick() {
826
+ if (!wsConnected) {
827
+ showSetupGuide();
828
+ } else {
829
+ showToast("Bridge active on port " + currentPort, "⚡");
502
830
  }
503
- showSetupGuide();
504
831
  }
505
832
 
506
833
  function showSetupGuide() {
@@ -511,41 +838,41 @@
511
838
  overlay.id = "setup-overlay";
512
839
  Object.assign(overlay.style, {
513
840
  position: "fixed", inset: "0",
514
- background: "rgba(9,9,15,0.96)",
515
- backdropFilter: "blur(4px)",
841
+ background: "rgba(9,10,15,0.96)",
842
+ backdropFilter: "blur(8px)",
516
843
  zIndex: "999",
517
844
  display: "flex", flexDirection: "column",
518
845
  alignItems: "center", justifyContent: "center",
519
- padding: "18px", gap: "10px",
520
- fontFamily: "-apple-system, BlinkMacSystemFont, 'Inter', sans-serif"
846
+ padding: "16px", gap: "10px",
847
+ fontFamily: "var(--font-ui)"
521
848
  });
522
849
 
523
850
  overlay.innerHTML = [
524
- '<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">',
851
+ '<div style="width:38px;height:38px;border-radius:9px;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">',
525
852
  '<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>',
526
853
  '</div>',
527
- '<div style="font-size:14px;font-weight:700;color:#f0f2f8;letter-spacing:-0.2px">Start Figma MCP Server</div>',
528
- '<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>',
854
+ '<div style="font-size:13px;font-weight:700;color:#f8fafc;letter-spacing:-0.2px">Start Figma MCP Server</div>',
855
+ '<div style="font-size:10px;color:#94a3b8;text-align:center;max-width:240px;line-height:1.5">Run command in your project terminal:</div>',
529
856
 
530
- '<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">',
531
- '<div style="font-size:8.5px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:#4e5873">Option A · Run once</div>',
532
- '<div style="font-family:\'SF Mono\',monospace;font-size:12px;font-weight:600;color:#38bdf8;user-select:text">npx figma-mcp</div>',
857
+ '<div style="width:100%;background:#0f1118;border:1px solid rgba(255,255,255,0.08);border-radius:8px;padding:9px 11px;display:flex;flex-direction:column;gap:3px">',
858
+ '<div style="font-size:8px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:#475569">Option A · Run once</div>',
859
+ '<div style="font-family:\'SF Mono\',monospace;font-size:11.5px;font-weight:600;color:#38bdf8;user-select:text">npx figma-mcp</div>',
533
860
  '</div>',
534
861
 
535
- '<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">',
536
- '<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>',
537
- '<div style="font-family:\'SF Mono\',monospace;font-size:12px;font-weight:600;color:#22d3a0;user-select:text">npx figma-mcp --install-service</div>',
538
- '<div style="font-size:9px;color:#4e5873">Sets up LaunchAgent / systemd / Task Scheduler</div>',
862
+ '<div style="width:100%;background:#0f1118;border:1px solid rgba(16,185,129,0.2);border-radius:8px;padding:9px 11px;display:flex;flex-direction:column;gap:3px">',
863
+ '<div style="font-size:8px;font-weight:700;letter-spacing:0.8px;text-transform:uppercase;color:#475569">Option B · Auto-start ✦ Recommended</div>',
864
+ '<div style="font-family:\'SF Mono\',monospace;font-size:11.5px;font-weight:600;color:#10b981;user-select:text">npx figma-mcp --install-service</div>',
865
+ '<div style="font-size:8.5px;color:#475569">Background service for Claude / IDE</div>',
539
866
  '</div>',
540
867
 
541
- '<div style="display:flex;align-items:center;gap:6px;font-size:9.5px;color:#4e5873">',
542
- '<div style="width:5px;height:5px;border-radius:50%;background:#f59e0b;animation:blink 1.4s ease-in-out infinite"></div>',
868
+ '<div style="display:flex;align-items:center;gap:6px;font-size:9px;color:#475569">',
869
+ '<div style="width:5px;height:5px;border-radius:50%;background:#f59e0b;animation:pulse 1.4s ease-in-out infinite"></div>',
543
870
  'Plugin auto-connects once server is detected',
544
871
  '</div>',
545
872
 
546
873
  '<button onclick="document.getElementById(\'setup-overlay\').remove();reconnect();" style="',
547
- 'margin-top:4px;padding:7px 20px;border-radius:8px;border:1px solid rgba(255,255,255,0.1);',
548
- 'background:rgba(255,255,255,0.05);color:#8b95b0;font-size:10.5px;font-weight:600;cursor:pointer;',
874
+ 'margin-top:4px;padding:6px 18px;border-radius:6px;border:1px solid rgba(255,255,255,0.12);',
875
+ 'background:rgba(255,255,255,0.06);color:#f8fafc;font-size:10px;font-weight:600;cursor:pointer;',
549
876
  'font-family:inherit;letter-spacing:0.1px',
550
877
  '">Dismiss & Retry</button>'
551
878
  ].join("");
@@ -571,30 +898,46 @@
571
898
  }, 500);
572
899
  }
573
900
 
574
- function log(msg, cls) {
901
+ function log(msg, cls, opType) {
575
902
  totalLogEvents++;
576
- if (logCountEl) logCountEl.textContent = totalLogEvents + " events";
903
+ if (logCountEl) logCountEl.textContent = totalLogEvents + " ev";
577
904
 
578
905
  if (logEmptyEl) logEmptyEl.style.display = "none";
579
906
 
907
+ var wasNearBottom = (logEl.scrollHeight - logEl.clientHeight - logEl.scrollTop) <= 45;
908
+
580
909
  var line = document.createElement("div");
581
910
  line.className = "log-line";
911
+ if (opType) line.dataset.type = opType;
582
912
 
583
913
  var ts = document.createElement("span");
584
914
  ts.className = "log-ts";
585
915
  ts.textContent = new Date().toLocaleTimeString("en", { hour12: false });
586
916
 
917
+ var prompt = document.createElement("span");
918
+ prompt.className = "log-prompt";
919
+ prompt.textContent = "❯";
920
+
587
921
  var content = document.createElement("span");
588
922
  content.className = "log-msg" + (cls ? " " + cls : "");
589
923
  content.textContent = msg;
590
924
 
591
925
  line.appendChild(ts);
926
+ line.appendChild(prompt);
592
927
  line.appendChild(content);
928
+
929
+ if (activeLogFilter === 'writes' && opType !== 'write') {
930
+ line.style.display = "none";
931
+ } else if (activeLogFilter === 'reads' && opType !== 'read') {
932
+ line.style.display = "none";
933
+ }
934
+
593
935
  logEl.appendChild(line);
594
- logEl.scrollTop = logEl.scrollHeight;
595
936
 
596
- // Trim log buffer: remove oldest entries in one batch DOM op to avoid
597
- // triggering O(N) reflows from individual removeChild calls.
937
+ if (wasNearBottom) {
938
+ logEl.scrollTop = logEl.scrollHeight;
939
+ }
940
+
598
941
  if (logEl.children.length > 90) {
599
942
  var keep = Array.from(logEl.children).slice(-70);
600
943
  logEl.replaceChildren.apply(logEl, keep);
@@ -604,11 +947,11 @@
604
947
  function clearLog() {
605
948
  logEl.innerHTML = "";
606
949
  totalLogEvents = 0;
607
- if (logCountEl) logCountEl.textContent = "0 events";
950
+ if (logCountEl) logCountEl.textContent = "0 ev";
608
951
  var empty = document.createElement("div");
609
952
  empty.id = "log-empty";
610
953
  empty.className = "terminal-empty";
611
- empty.innerHTML = '<div class="terminal-empty-icon">◌</div><div class="terminal-empty-text">Waiting for events…</div>';
954
+ empty.innerHTML = '<div class="terminal-empty-icon">◌</div><div class="terminal-empty-text">Listening on MCP bridge…</div>';
612
955
  logEl.appendChild(empty);
613
956
  logEmptyEl = empty;
614
957
  }
@@ -626,13 +969,11 @@
626
969
  var isLongPolling = false;
627
970
  var activeAbortController = null;
628
971
  var isFirstPoll = true;
629
-
630
972
  var retryTimer = null;
631
973
 
632
974
  async function initConnection() {
633
975
  if (wsConnected) return;
634
976
 
635
- // 1. Instant Health Check on localhost (< 2ms)
636
977
  try {
637
978
  var res = await fetch("http://localhost:" + currentPort + "/health", { signal: AbortSignal.timeout(600) });
638
979
  if (res.ok) {
@@ -640,13 +981,12 @@
640
981
  everConnected = true;
641
982
  consecutiveErrors = 0;
642
983
  lastSuccessfulPollAt = Date.now();
643
- setStatus("green", "Connected", "ready");
984
+ setStatus("green", "Connected");
644
985
  connectWs();
645
986
  return;
646
987
  }
647
988
  } catch(e) {}
648
989
 
649
- // 2. Parallel port scan 38451-38455 — all probes fire simultaneously
650
990
  var scanPorts = [38451, 38452, 38453, 38454, 38455].filter(function(p) { return p !== currentPort; });
651
991
  try {
652
992
  var winner = await Promise.any(scanPorts.map(function(p) {
@@ -655,18 +995,15 @@
655
995
  }));
656
996
  currentPort = winner;
657
997
  BRIDGE = "http://localhost:" + winner;
658
- var pDisp = document.getElementById("port-display");
659
- if (pDisp) pDisp.textContent = winner;
660
998
  everConnected = true;
661
999
  consecutiveErrors = 0;
662
1000
  lastSuccessfulPollAt = Date.now();
663
- setStatus("green", "Connected", "ready");
1001
+ setStatus("green", "Connected");
664
1002
  connectWs();
665
1003
  return;
666
- } catch(_) { /* no server yet */ }
1004
+ } catch(_) {}
667
1005
 
668
- // 3. If server is not yet running, schedule continuous auto-detection every 1.5s
669
- setStatus("yellow", "Waiting for server…");
1006
+ setStatus("yellow", "Waiting…");
670
1007
  if (retryTimer) clearTimeout(retryTimer);
671
1008
  retryTimer = setTimeout(function() {
672
1009
  if (!wsConnected && polling) {
@@ -703,8 +1040,8 @@
703
1040
  everConnected = true;
704
1041
  if (retryTimer) clearTimeout(retryTimer);
705
1042
  lastSuccessfulPollAt = Date.now();
706
- setStatus("green", "Connected", "ready");
707
- log("⚡ Connected to Figma MCP · port " + currentPort, "ok");
1043
+ setStatus("green", "Connected");
1044
+ log("Connected to Figma MCP · port " + currentPort, "ok");
708
1045
 
709
1046
  if (pingTimer) clearInterval(pingTimer);
710
1047
  pingTimer = setInterval(function() {
@@ -720,7 +1057,8 @@
720
1057
  try {
721
1058
  var req = JSON.parse(event.data);
722
1059
  if (req && req.operation) {
723
- log("→ " + req.operation, READ_OPS.indexOf(req.operation) >= 0 ? "info" : "ok");
1060
+ var isRead = READ_OPS.indexOf(req.operation) >= 0;
1061
+ log("→ " + req.operation, isRead ? "info" : "ok", isRead ? "read" : "write");
724
1062
  try { ws.send(JSON.stringify({ type: "ack", id: req.id })); } catch(e) {}
725
1063
  dispatchToMain(req);
726
1064
  }
@@ -739,7 +1077,6 @@
739
1077
  if (pingTimer) clearInterval(pingTimer);
740
1078
  wsConnected = false;
741
1079
  if (polling) {
742
- // Attempt long-poll fallback and schedule retry
743
1080
  startLongPoll();
744
1081
  if (retryTimer) clearTimeout(retryTimer);
745
1082
  retryTimer = setTimeout(function() {
@@ -791,7 +1128,7 @@
791
1128
  var body = await res.json();
792
1129
  var reqs = body.requests || [];
793
1130
  receivedThisRound = reqs.length;
794
- setStatus("green", reqs.length ? reqs.length + " cmd" : "Connected", body.mode || "ready");
1131
+ setStatus("green", reqs.length ? reqs.length + " cmd" : "Connected");
795
1132
 
796
1133
  consecutiveErrors = 0;
797
1134
  everConnected = true;
@@ -799,7 +1136,8 @@
799
1136
 
800
1137
  for (var i = 0; i < reqs.length; i++) {
801
1138
  var req = reqs[i];
802
- log("→ " + req.operation, READ_OPS.indexOf(req.operation) >= 0 ? "info" : "ok");
1139
+ var isRead = READ_OPS.indexOf(req.operation) >= 0;
1140
+ log("→ " + req.operation, isRead ? "info" : "ok", isRead ? "read" : "write");
803
1141
  dispatchToMain(req);
804
1142
  }
805
1143
  } catch (e) {
@@ -816,9 +1154,6 @@
816
1154
  await sleep(backoff);
817
1155
  continue;
818
1156
  }
819
- // Re-poll immediately after a round that carried work: the next op in
820
- // a read chain is usually already queued, and sleeping here added
821
- // POLL_MS of dead time to every single round-trip.
822
1157
  if (!receivedThisRound) await sleep(POLL_MS);
823
1158
  }
824
1159
  } finally {
@@ -826,9 +1161,6 @@
826
1161
  }
827
1162
  }
828
1163
 
829
- // dataJson is already-serialized JSON produced by the main thread, so it is
830
- // spliced into the body as-is — a multi-MB design tree is never re-encoded
831
- // here. Older builds of code.js send a plain `data` object instead.
832
1164
  function buildResponseBody(id, success, dataJson, error) {
833
1165
  return '{"id":' + JSON.stringify(id) +
834
1166
  ',"success":' + (success ? "true" : "false") +
@@ -847,7 +1179,6 @@
847
1179
  } catch(e) {}
848
1180
  }
849
1181
 
850
- // HTTP fallback
851
1182
  try {
852
1183
  await fetch(BRIDGE + "/response", {
853
1184
  method: "POST",
@@ -881,59 +1212,86 @@
881
1212
  }, 100);
882
1213
  }
883
1214
 
884
- document.addEventListener("visibilitychange", function() {
885
- if (document.visibilityState !== "visible" || !everConnected) return;
886
- var idleMs = Date.now() - lastSuccessfulPollAt;
887
- if (!wsConnected || consecutiveErrors > 0 || activeAbortController || idleMs > 20000) {
888
- log("Focus sync (" + Math.round(idleMs / 1000) + "s)", "info");
889
- reconnect();
1215
+ function sendActivePing() {
1216
+ if (ws && ws.readyState === WebSocket.OPEN) {
1217
+ try {
1218
+ ws.send(JSON.stringify({ type: "ping", active: true, sessionId: sessionId, fileName: fileName }));
1219
+ } catch(e) {}
890
1220
  }
891
- });
1221
+ }
1222
+
1223
+ window.addEventListener("focus", sendActivePing);
1224
+ document.addEventListener("pointerdown", sendActivePing);
892
1225
 
893
- window.addEventListener("focus", function() {
1226
+ document.addEventListener("visibilitychange", function() {
1227
+ if (document.visibilityState !== "visible") return;
1228
+ sendActivePing();
894
1229
  if (!everConnected) return;
895
1230
  var idleMs = Date.now() - lastSuccessfulPollAt;
896
1231
  if (!wsConnected || consecutiveErrors > 0 || activeAbortController || idleMs > 20000) {
897
- log("Window focus sync", "info");
1232
+ log("Focus sync (" + Math.round(idleMs / 1000) + "s)", "info");
898
1233
  reconnect();
899
1234
  }
900
1235
  });
901
1236
 
902
- function changePort() {
903
- var input = prompt("MCP Bridge Port:", currentPort);
904
- if (!input) return;
905
- var newPort = parseInt(input, 10);
906
- if (isNaN(newPort) || newPort < 1024 || newPort > 65535) {
907
- alert("Invalid port number");
908
- return;
1237
+ function getIconForType(type) {
1238
+ if (!type) return '<rect x="3" y="3" width="18" height="18" rx="2"/>';
1239
+ var t = type.toUpperCase();
1240
+ if (t === "FRAME" || t === "SECTION") {
1241
+ return '<rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 3v18M15 3v18M3 9h18M3 15h18"/>';
909
1242
  }
910
- setPort(newPort);
911
- }
912
-
913
- function setPort(port) {
914
- currentPort = port;
915
- BRIDGE = "http://localhost:" + currentPort;
916
- document.getElementById("port-display").textContent = currentPort;
917
- log("Port switched → " + currentPort, "info");
918
- reconnect();
1243
+ if (t === "COMPONENT" || t === "INSTANCE") {
1244
+ return '<path d="M12 2L2 12l10 10 10-10L12 2z"/>';
1245
+ }
1246
+ if (t === "TEXT") {
1247
+ return '<polyline points="4 7 4 4 20 4 20 7"/><line x1="9" y1="20" x2="15" y2="20"/><line x1="12" y1="4" x2="12" y2="20"/>';
1248
+ }
1249
+ if (t === "GROUP") {
1250
+ return '<rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>';
1251
+ }
1252
+ return '<rect x="3" y="3" width="18" height="18" rx="2"/>';
919
1253
  }
920
1254
 
921
- async function autoDetectPort() {
922
- setStatus("yellow", "Scanning…");
923
- log("Scanning 38451-38460…", "info");
924
- for (var p = 38451; p <= 38460; p++) {
925
- try {
926
- var res = await fetch("http://localhost:" + p + "/health", { signal: AbortSignal.timeout(400) });
927
- if (res.ok) {
928
- var data = await res.json();
929
- log("Detected port " + p, "ok");
930
- setPort(p);
931
- return;
932
- }
933
- } catch(e) {}
1255
+ function renderSelection(msg) {
1256
+ if (!selCardEl || !selNameEl || !selMetaEl || !selBadgeEl || !selIconEl) return;
1257
+
1258
+ var count = msg.count || 0;
1259
+ var list = msg.selection || [];
1260
+ currentSelectionCount = count;
1261
+ currentSelectionId = (count === 1 && list[0]) ? list[0].id : null;
1262
+ currentSelectionName = (count === 1 && list[0]) ? (list[0].name || "Layer") : "";
1263
+
1264
+ if (count === 0) {
1265
+ selCardEl.classList.remove("has-selection");
1266
+ selIconEl.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/></svg>';
1267
+ selNameEl.textContent = "No selection";
1268
+ selMetaEl.textContent = msg.pageName ? "Page: " + msg.pageName : (fileName || "Canvas root");
1269
+ if (selActionsEl) selActionsEl.style.display = "none";
1270
+ selCardEl.title = "No layer selected in Figma canvas";
1271
+ } else if (count === 1) {
1272
+ var node = list[0];
1273
+ selCardEl.classList.add("has-selection");
1274
+ selIconEl.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + getIconForType(node.type) + '</svg>';
1275
+ selNameEl.textContent = node.name || "Untitled Node";
1276
+
1277
+ var metaParts = [];
1278
+ if (node.width && node.height) metaParts.push(node.width + " × " + node.height);
1279
+ if (msg.pageName) metaParts.push(msg.pageName);
1280
+ selMetaEl.textContent = metaParts.length ? metaParts.join(" · ") : (node.type || "Layer");
1281
+
1282
+ selBadgeEl.textContent = node.type || "NODE";
1283
+ if (selActionsEl) selActionsEl.style.display = "flex";
1284
+ selCardEl.title = "Click to center view on: " + (node.name || "Node") + " (" + (node.type || "NODE") + " ID: " + node.id + ")";
1285
+ } else {
1286
+ selCardEl.classList.add("has-selection");
1287
+ selIconEl.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>';
1288
+ var names = list.slice(0, 2).map(function(n) { return n.name; }).join(", ");
1289
+ selNameEl.textContent = count + " layers selected";
1290
+ selMetaEl.textContent = names + (count > 2 ? " +" + (count - 2) + " more" : "");
1291
+ selBadgeEl.textContent = count + " ITEMS";
1292
+ if (selActionsEl) selActionsEl.style.display = "flex";
1293
+ selCardEl.title = "Click to center view on " + count + " selected layers";
934
1294
  }
935
- log("Not found on 38451-38460 → Reset 38451", "err");
936
- setPort(38451);
937
1295
  }
938
1296
 
939
1297
  window.onmessage = async function(event) {
@@ -944,8 +1302,9 @@
944
1302
  var prevSid = sessionId;
945
1303
  sessionId = msg.sessionId || null;
946
1304
  fileName = msg.fileName || null;
947
- if (fileName && sessionDispEl) {
948
- sessionDispEl.textContent = fileName.length > 16 ? fileName.slice(0, 14) + ".." : fileName;
1305
+ if (fileName && docDispEl) {
1306
+ docDispEl.textContent = fileName;
1307
+ docDispEl.title = fileName + " (" + (sessionId || "active session") + ")";
949
1308
  }
950
1309
  if (sessionId) log("Linked: " + (fileName || sessionId), "info");
951
1310
  if (sessionId && sessionId !== prevSid) {
@@ -955,14 +1314,53 @@
955
1314
  }
956
1315
 
957
1316
  if (msg.type === "selection-change") {
958
- if (sessionDispEl) {
959
- if (msg.count === 0) {
960
- sessionDispEl.textContent = fileName ? (fileName.length > 16 ? fileName.slice(0, 14) + ".." : fileName) : "READY";
961
- } else if (msg.count === 1) {
962
- var nName = msg.selection[0].name;
963
- sessionDispEl.textContent = "Sel: " + (nName.length > 12 ? nName.slice(0, 10) + ".." : nName);
964
- } else {
965
- sessionDispEl.textContent = msg.count + " layers selected";
1317
+ renderSelection(msg);
1318
+ if (ws && ws.readyState === WebSocket.OPEN) {
1319
+ try {
1320
+ ws.send(JSON.stringify({
1321
+ type: "selection-change",
1322
+ sessionId: sessionId,
1323
+ count: msg.count,
1324
+ pageName: msg.pageName,
1325
+ selection: msg.selection,
1326
+ fullNode: msg.fullNode
1327
+ }));
1328
+ } catch(e) {}
1329
+ }
1330
+ sendActivePing();
1331
+ return;
1332
+ }
1333
+
1334
+ if (msg.type === "delta-diff") {
1335
+ if (ws && ws.readyState === WebSocket.OPEN) {
1336
+ try {
1337
+ ws.send(JSON.stringify({
1338
+ type: "delta-diff",
1339
+ sessionId: sessionId,
1340
+ id: msg.id,
1341
+ diff: msg.diff
1342
+ }));
1343
+ } catch(e) {}
1344
+ }
1345
+ return;
1346
+ }
1347
+
1348
+ if (msg.type === "selection-image-exported") {
1349
+ if (snapshotBtn) snapshotBtn.style.opacity = "1";
1350
+ if (msg.bytes && msg.bytes.length > 0) {
1351
+ try {
1352
+ var blob = new Blob([new Uint8Array(msg.bytes)], { type: "image/png" });
1353
+ if (navigator && navigator.clipboard && navigator.clipboard.write) {
1354
+ await navigator.clipboard.write([
1355
+ new ClipboardItem({ "image/png": blob })
1356
+ ]);
1357
+ showToast("PNG Copied to Clipboard (2x)", "📸");
1358
+ log("📸 Exported PNG for " + (msg.nodeName || "node"), "ok", "read");
1359
+ } else {
1360
+ showToast("Image captured", "📸");
1361
+ }
1362
+ } catch(err) {
1363
+ showToast("Failed to copy image", "✗");
966
1364
  }
967
1365
  }
968
1366
  return;
@@ -990,7 +1388,9 @@
990
1388
  }
991
1389
 
992
1390
  if (msg.type === "index-update") {
993
- if (reindexBtnLabel) reindexBtnLabel.textContent = "⚡ Indexed";
1391
+ if (reindexBtn) reindexBtn.classList.remove("indexing");
1392
+ var elapsed = msg.startMs ? (Date.now() - msg.startMs) : 0;
1393
+ showToast("Indexed in " + elapsed + "ms", "✓");
994
1394
  if (ws && ws.readyState === WebSocket.OPEN) {
995
1395
  try {
996
1396
  ws.send(JSON.stringify({
@@ -1000,13 +1400,9 @@
1000
1400
  sessionId: msg.sessionId,
1001
1401
  startMs: msg.startMs
1002
1402
  }));
1003
- var nodeCount = (msg.data && msg.data.pageNodes) ? msg.data.pageNodes.length : 0;
1004
- log("⚡ Indexed " + (msg.fileName || "file") + " (" + (Date.now() - msg.startMs) + "ms)", "ok");
1403
+ log("Indexed " + (msg.fileName || "canvas") + " (" + elapsed + "ms)", "ok", "read");
1005
1404
  } catch(e) {}
1006
1405
  }
1007
- setTimeout(function() {
1008
- if (reindexBtnLabel) reindexBtnLabel.textContent = "⚡ Index";
1009
- }, 3000);
1010
1406
  return;
1011
1407
  }
1012
1408
 
@@ -1023,11 +1419,10 @@
1023
1419
  writeCount++;
1024
1420
  document.getElementById("write-count").textContent = writeCount;
1025
1421
  }
1026
- log("✓ " + (msg.id ? msg.id.slice(-6) : "ok") + durationStr, "ok");
1422
+ log("✓ " + (msg.id ? msg.id.slice(-6) : "ok") + durationStr, "ok", isRead ? "read" : "write");
1027
1423
  } else {
1028
1424
  errCount++;
1029
- document.getElementById("err-count").textContent = errCount;
1030
- log("✗ " + (msg.id ? msg.id.slice(-6) : "err") + durationStr + ": " + (msg.error || "fail"), "err");
1425
+ log("✗ " + (msg.id ? msg.id.slice(-6) : "err") + durationStr + ": " + (msg.error || "fail"), "err", isRead ? "read" : "write");
1031
1426
  }
1032
1427
  var payloadJson = msg.dataJson !== undefined
1033
1428
  ? msg.dataJson
@@ -1044,7 +1439,6 @@
1044
1439
 
1045
1440
  var rafId = null;
1046
1441
  var lastW = 0, lastH = 0;
1047
- var saveTimer = null;
1048
1442
 
1049
1443
  grip.addEventListener("pointerdown", function(e) {
1050
1444
  e.preventDefault();