@mp3wizard/figma-console-mcp 1.32.2 → 1.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -17
- package/dist/cloudflare/core/cloud-websocket-connector.js +18 -0
- package/dist/cloudflare/core/design-code-tools.js +60 -17
- package/dist/cloudflare/core/design-system-manifest.js +19 -14
- package/dist/cloudflare/core/design-system-tools.js +43 -34
- package/dist/cloudflare/core/diagnose-tool.js +4 -0
- package/dist/cloudflare/core/enrichment/enrichment-service.js +11 -5
- package/dist/cloudflare/core/enrichment/style-resolver.js +38 -18
- package/dist/cloudflare/core/figma-api.js +118 -54
- package/dist/cloudflare/core/figma-tools.js +179 -63
- package/dist/cloudflare/core/port-discovery.js +404 -31
- package/dist/cloudflare/core/tokens/alias-resolver.js +75 -5
- package/dist/cloudflare/core/tokens/config.js +10 -0
- package/dist/cloudflare/core/tokens/dialect.js +232 -0
- package/dist/cloudflare/core/tokens/figma-converter.js +144 -16
- package/dist/cloudflare/core/tokens/formatters/css-vars.js +21 -12
- package/dist/cloudflare/core/tokens/formatters/dtcg.js +106 -30
- package/dist/cloudflare/core/tokens/formatters/json.js +28 -10
- package/dist/cloudflare/core/tokens/formatters/scss.js +19 -13
- package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +15 -9
- package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +14 -9
- package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +11 -5
- package/dist/cloudflare/core/tokens/index.js +2 -1
- package/dist/cloudflare/core/tokens/parsers/dtcg.js +32 -5
- package/dist/cloudflare/core/tokens/schemas.js +4 -0
- package/dist/cloudflare/core/tokens-tools.js +1017 -88
- package/dist/cloudflare/core/version-tools.js +44 -3
- package/dist/cloudflare/core/websocket-connector.js +42 -0
- package/dist/cloudflare/core/websocket-server.js +99 -8
- package/dist/cloudflare/core/write-tools.js +355 -86
- package/dist/cloudflare/index.js +7 -7
- package/dist/core/design-code-tools.d.ts.map +1 -1
- package/dist/core/design-code-tools.js +60 -17
- package/dist/core/design-code-tools.js.map +1 -1
- package/dist/core/design-system-manifest.d.ts +1 -0
- package/dist/core/design-system-manifest.d.ts.map +1 -1
- package/dist/core/design-system-manifest.js +19 -14
- package/dist/core/design-system-manifest.js.map +1 -1
- package/dist/core/design-system-tools.d.ts.map +1 -1
- package/dist/core/design-system-tools.js +43 -34
- package/dist/core/design-system-tools.js.map +1 -1
- package/dist/core/diagnose-tool.d.ts +8 -0
- package/dist/core/diagnose-tool.d.ts.map +1 -1
- package/dist/core/diagnose-tool.js +4 -0
- package/dist/core/diagnose-tool.js.map +1 -1
- package/dist/core/enrichment/enrichment-service.d.ts.map +1 -1
- package/dist/core/enrichment/enrichment-service.js +11 -5
- package/dist/core/enrichment/enrichment-service.js.map +1 -1
- package/dist/core/enrichment/style-resolver.d.ts +7 -2
- package/dist/core/enrichment/style-resolver.d.ts.map +1 -1
- package/dist/core/enrichment/style-resolver.js +38 -18
- package/dist/core/enrichment/style-resolver.js.map +1 -1
- package/dist/core/figma-api.d.ts +18 -9
- package/dist/core/figma-api.d.ts.map +1 -1
- package/dist/core/figma-api.js +118 -54
- package/dist/core/figma-api.js.map +1 -1
- package/dist/core/figma-connector.d.ts +12 -0
- package/dist/core/figma-connector.d.ts.map +1 -1
- package/dist/core/figma-tools.d.ts.map +1 -1
- package/dist/core/figma-tools.js +179 -63
- package/dist/core/figma-tools.js.map +1 -1
- package/dist/core/port-discovery.d.ts +40 -0
- package/dist/core/port-discovery.d.ts.map +1 -1
- package/dist/core/port-discovery.js +404 -31
- package/dist/core/port-discovery.js.map +1 -1
- package/dist/core/tokens/alias-resolver.d.ts +45 -3
- package/dist/core/tokens/alias-resolver.d.ts.map +1 -1
- package/dist/core/tokens/alias-resolver.js +75 -5
- package/dist/core/tokens/alias-resolver.js.map +1 -1
- package/dist/core/tokens/config.d.ts +28 -0
- package/dist/core/tokens/config.d.ts.map +1 -1
- package/dist/core/tokens/config.js +10 -0
- package/dist/core/tokens/config.js.map +1 -1
- package/dist/core/tokens/dialect.d.ts +107 -0
- package/dist/core/tokens/dialect.d.ts.map +1 -0
- package/dist/core/tokens/dialect.js +233 -0
- package/dist/core/tokens/dialect.js.map +1 -0
- package/dist/core/tokens/figma-converter.d.ts +23 -2
- package/dist/core/tokens/figma-converter.d.ts.map +1 -1
- package/dist/core/tokens/figma-converter.js +144 -16
- package/dist/core/tokens/figma-converter.js.map +1 -1
- package/dist/core/tokens/formatters/css-vars.d.ts.map +1 -1
- package/dist/core/tokens/formatters/css-vars.js +21 -12
- package/dist/core/tokens/formatters/css-vars.js.map +1 -1
- package/dist/core/tokens/formatters/dtcg.d.ts +2 -2
- package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -1
- package/dist/core/tokens/formatters/dtcg.js +106 -30
- package/dist/core/tokens/formatters/dtcg.js.map +1 -1
- package/dist/core/tokens/formatters/json.d.ts.map +1 -1
- package/dist/core/tokens/formatters/json.js +28 -10
- package/dist/core/tokens/formatters/json.js.map +1 -1
- package/dist/core/tokens/formatters/scss.d.ts.map +1 -1
- package/dist/core/tokens/formatters/scss.js +19 -13
- package/dist/core/tokens/formatters/scss.js.map +1 -1
- package/dist/core/tokens/formatters/style-dictionary-v3.d.ts.map +1 -1
- package/dist/core/tokens/formatters/style-dictionary-v3.js +15 -9
- package/dist/core/tokens/formatters/style-dictionary-v3.js.map +1 -1
- package/dist/core/tokens/formatters/tailwind-v4.d.ts.map +1 -1
- package/dist/core/tokens/formatters/tailwind-v4.js +14 -9
- package/dist/core/tokens/formatters/tailwind-v4.js.map +1 -1
- package/dist/core/tokens/formatters/tokens-studio.d.ts.map +1 -1
- package/dist/core/tokens/formatters/tokens-studio.js +11 -5
- package/dist/core/tokens/formatters/tokens-studio.js.map +1 -1
- package/dist/core/tokens/index.d.ts +2 -1
- package/dist/core/tokens/index.d.ts.map +1 -1
- package/dist/core/tokens/index.js +2 -1
- package/dist/core/tokens/index.js.map +1 -1
- package/dist/core/tokens/parsers/dtcg.js +32 -5
- package/dist/core/tokens/parsers/dtcg.js.map +1 -1
- package/dist/core/tokens/schemas.d.ts +3 -0
- package/dist/core/tokens/schemas.d.ts.map +1 -1
- package/dist/core/tokens/schemas.js +4 -0
- package/dist/core/tokens/schemas.js.map +1 -1
- package/dist/core/tokens/types.d.ts +57 -1
- package/dist/core/tokens/types.d.ts.map +1 -1
- package/dist/core/tokens/types.js.map +1 -1
- package/dist/core/tokens-tools.d.ts +250 -7
- package/dist/core/tokens-tools.d.ts.map +1 -1
- package/dist/core/tokens-tools.js +1017 -88
- package/dist/core/tokens-tools.js.map +1 -1
- package/dist/core/version-tools.d.ts.map +1 -1
- package/dist/core/version-tools.js +44 -3
- package/dist/core/version-tools.js.map +1 -1
- package/dist/core/websocket-connector.d.ts +38 -0
- package/dist/core/websocket-connector.d.ts.map +1 -1
- package/dist/core/websocket-connector.js +42 -0
- package/dist/core/websocket-connector.js.map +1 -1
- package/dist/core/websocket-server.d.ts +23 -0
- package/dist/core/websocket-server.d.ts.map +1 -1
- package/dist/core/websocket-server.js +99 -8
- package/dist/core/websocket-server.js.map +1 -1
- package/dist/core/write-tools.d.ts.map +1 -1
- package/dist/core/write-tools.js +355 -86
- package/dist/core/write-tools.js.map +1 -1
- package/dist/local.d.ts +0 -1
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +253 -63
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/code.js +382 -28
- package/figma-desktop-bridge/ui.html +578 -292
- package/package.json +2 -2
|
@@ -66,6 +66,10 @@
|
|
|
66
66
|
align-items: center;
|
|
67
67
|
gap: 5px;
|
|
68
68
|
margin-right: 3px;
|
|
69
|
+
/* Take the free space so long status sentences ellipsize gracefully
|
|
70
|
+
instead of pushing the CTA/icons off the 240px canvas. */
|
|
71
|
+
flex: 1;
|
|
72
|
+
min-width: 0;
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
.status-indicator {
|
|
@@ -90,18 +94,27 @@
|
|
|
90
94
|
background: var(--color-error);
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
/* Deliberate user pause — static grey dot, NOT the pulsing loading
|
|
98
|
+
animation, so a pause never reads as "trying to connect". */
|
|
99
|
+
.status-indicator.paused {
|
|
100
|
+
background: var(--color-idle);
|
|
101
|
+
}
|
|
102
|
+
|
|
93
103
|
@keyframes pulse {
|
|
94
104
|
0%, 100% { opacity: 0.4; }
|
|
95
105
|
50% { opacity: 1; }
|
|
96
106
|
}
|
|
97
107
|
|
|
98
108
|
#status-state {
|
|
99
|
-
font-weight:
|
|
109
|
+
font-weight: 600;
|
|
100
110
|
font-size: 10px;
|
|
101
111
|
letter-spacing: -0.2px;
|
|
102
|
-
text-transform: uppercase;
|
|
103
112
|
font-stretch: 75%;
|
|
104
113
|
color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.6));
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
text-overflow: ellipsis;
|
|
116
|
+
white-space: nowrap;
|
|
117
|
+
min-width: 0;
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
.status-indicator.active + #status-state {
|
|
@@ -134,8 +147,24 @@
|
|
|
134
147
|
background: var(--figma-color-bg-secondary, #383838);
|
|
135
148
|
}
|
|
136
149
|
|
|
137
|
-
|
|
138
|
-
|
|
150
|
+
/* ===== Status meta line — connection count + proof of life ===== */
|
|
151
|
+
.status-meta {
|
|
152
|
+
display: none; /* toggled to flex from JS when there's something to show */
|
|
153
|
+
align-items: center;
|
|
154
|
+
gap: 8px;
|
|
155
|
+
padding-left: 17px; /* aligns with the pill text (4px row offset + 8px dot + 5px gap) */
|
|
156
|
+
font-size: 9px;
|
|
157
|
+
color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.5));
|
|
158
|
+
white-space: nowrap;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Visible recovery instruction for the ERROR state */
|
|
162
|
+
.status-error-hint {
|
|
163
|
+
display: none; /* shown from JS while in the error state */
|
|
164
|
+
padding-left: 17px;
|
|
165
|
+
font-size: 10px;
|
|
166
|
+
line-height: 1.4;
|
|
167
|
+
color: var(--color-error);
|
|
139
168
|
}
|
|
140
169
|
|
|
141
170
|
.icon-btn {
|
|
@@ -320,43 +349,24 @@
|
|
|
320
349
|
height: 11px;
|
|
321
350
|
}
|
|
322
351
|
|
|
323
|
-
/* ===== Row:
|
|
324
|
-
.
|
|
325
|
-
flex-direction: row;
|
|
352
|
+
/* ===== Row: plugin update banner ===== */
|
|
353
|
+
.update-banner {
|
|
326
354
|
align-items: center;
|
|
327
|
-
gap:
|
|
328
|
-
padding: 4px
|
|
355
|
+
gap: 6px;
|
|
356
|
+
padding: 4px 6px;
|
|
357
|
+
background: var(--figma-color-bg-secondary, #383838);
|
|
358
|
+
border: 1px solid var(--figma-color-border, #4a4a4a);
|
|
359
|
+
border-radius: 3px;
|
|
329
360
|
font-size: 10px;
|
|
330
|
-
|
|
361
|
+
line-height: 1.4;
|
|
362
|
+
color: var(--figma-color-text, rgba(255, 255, 255, 0.9));
|
|
331
363
|
}
|
|
332
364
|
|
|
333
|
-
.
|
|
334
|
-
display: flex;
|
|
335
|
-
flex-direction: column;
|
|
336
|
-
gap: 2px;
|
|
365
|
+
.update-banner span {
|
|
337
366
|
flex: 1;
|
|
338
367
|
min-width: 0;
|
|
339
368
|
}
|
|
340
369
|
|
|
341
|
-
.info-row {
|
|
342
|
-
display: flex;
|
|
343
|
-
gap: 6px;
|
|
344
|
-
overflow: hidden;
|
|
345
|
-
align-items: center;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
.info-row-label {
|
|
349
|
-
color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.5));
|
|
350
|
-
flex-shrink: 0;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
.info-row-value {
|
|
354
|
-
color: var(--figma-color-text, rgba(255, 255, 255, 0.9));
|
|
355
|
-
overflow: hidden;
|
|
356
|
-
text-overflow: ellipsis;
|
|
357
|
-
white-space: nowrap;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
370
|
/* ===== Row: log panel ===== */
|
|
361
371
|
.log-panel {
|
|
362
372
|
flex-direction: column;
|
|
@@ -367,16 +377,6 @@
|
|
|
367
377
|
background: var(--figma-color-bg, #1e1e1e);
|
|
368
378
|
}
|
|
369
379
|
|
|
370
|
-
.log-header {
|
|
371
|
-
display: flex;
|
|
372
|
-
justify-content: flex-end;
|
|
373
|
-
padding: 3px 6px;
|
|
374
|
-
font-size: 9px;
|
|
375
|
-
color: var(--figma-color-text-secondary, rgba(255, 255, 255, 0.5));
|
|
376
|
-
background: var(--figma-color-bg-secondary, #383838);
|
|
377
|
-
border-bottom: 1px solid var(--figma-color-border, #4a4a4a);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
380
|
.log-entries {
|
|
381
381
|
max-height: 160px;
|
|
382
382
|
overflow-y: auto;
|
|
@@ -386,10 +386,6 @@
|
|
|
386
386
|
line-height: 1.4;
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
.log-entries.errors-only .log-entry:not(.error) {
|
|
390
|
-
display: none;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
389
|
.log-entry {
|
|
394
390
|
display: flex;
|
|
395
391
|
align-items: baseline;
|
|
@@ -470,10 +466,10 @@
|
|
|
470
466
|
<div class="row-top">
|
|
471
467
|
<div class="status-pill">
|
|
472
468
|
<div class="status-indicator loading" id="status-dot" aria-hidden="true"></div>
|
|
473
|
-
<span id="status-state" role="status" aria-live="polite">
|
|
469
|
+
<span id="status-state" role="status" aria-live="polite">Looking for your AI app…</span>
|
|
474
470
|
</div>
|
|
475
|
-
|
|
476
|
-
<
|
|
471
|
+
<!-- CTA starts hidden; the first state reconcile shows it with an honest label -->
|
|
472
|
+
<button class="cta-btn" id="cta-btn" onclick="toggleLocalConnection()" style="display:none">Pause</button>
|
|
477
473
|
<button class="icon-btn" id="cloud-icon" onclick="toggleCloudPair()" title="Cloud pairing" aria-label="Cloud pairing" aria-expanded="false">
|
|
478
474
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
|
479
475
|
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/>
|
|
@@ -482,6 +478,21 @@
|
|
|
482
478
|
<button class="icon-btn" id="expand-btn" onclick="toggleSubToolbar()" title="Show options" aria-label="Show options" aria-expanded="false">+</button>
|
|
483
479
|
</div>
|
|
484
480
|
|
|
481
|
+
<!-- Status meta — connection count + proof of life (shown when connected) -->
|
|
482
|
+
<div class="status-meta" id="status-meta">
|
|
483
|
+
<span id="status-conn-count"></span>
|
|
484
|
+
<span id="status-last-action"></span>
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
<!-- Visible recovery instruction, shown only in the error state -->
|
|
488
|
+
<div class="status-error-hint" id="status-error-hint">Close this window and reopen the plugin.</div>
|
|
489
|
+
|
|
490
|
+
<!-- Plugin update banner (shown when the server reports a newer plugin) -->
|
|
491
|
+
<div class="row update-banner" id="update-banner">
|
|
492
|
+
<span>Plugin update available — re-import it in Figma (Plugins → Development → Import from manifest).</span>
|
|
493
|
+
<button class="icon-btn icon-btn--borderless" onclick="dismissUpdateBanner()" title="Dismiss" aria-label="Dismiss update notice">×</button>
|
|
494
|
+
</div>
|
|
495
|
+
|
|
485
496
|
<!-- Cloud pairing (shown when cloud icon active) -->
|
|
486
497
|
<div class="row cloud-pair" id="cloud-pair">
|
|
487
498
|
<button class="icon-btn icon-btn--borderless cloud-info-btn" id="cloud-info-btn" onclick="toggleCloudHelp()" title="About pairing codes" aria-label="About pairing codes" aria-expanded="false" aria-controls="cloud-help">
|
|
@@ -502,29 +513,14 @@
|
|
|
502
513
|
|
|
503
514
|
<!-- Sub-toolbar (shown when [+] active) -->
|
|
504
515
|
<div class="row sub-toolbar" id="sub-toolbar">
|
|
505
|
-
<button class="sub-btn" id="info-toggle" onclick="toggleInfo()" aria-expanded="false" aria-controls="info-panel">
|
|
506
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
|
507
|
-
<circle cx="12" cy="12" r="10"/>
|
|
508
|
-
<line x1="12" y1="16" x2="12" y2="12"/>
|
|
509
|
-
<line x1="12" y1="8" x2="12.01" y2="8"/>
|
|
510
|
-
</svg>
|
|
511
|
-
Info
|
|
512
|
-
</button>
|
|
513
516
|
<button class="sub-btn" id="log-toggle" onclick="toggleLog()" aria-expanded="false" aria-controls="log-panel">
|
|
514
517
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
|
515
518
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
|
516
519
|
<circle cx="12" cy="12" r="3"/>
|
|
517
520
|
</svg>
|
|
518
|
-
|
|
521
|
+
Activity
|
|
519
522
|
</button>
|
|
520
|
-
<button class="icon-btn" id="
|
|
521
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
|
522
|
-
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
|
523
|
-
<line x1="12" y1="9" x2="12" y2="13"/>
|
|
524
|
-
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
|
525
|
-
</svg>
|
|
526
|
-
</button>
|
|
527
|
-
<button class="icon-btn" id="copy-log-btn" onclick="copyLogToClipboard()" title="Copy log to clipboard" aria-label="Copy log to clipboard" style="display:none">
|
|
523
|
+
<button class="icon-btn" id="copy-log-btn" onclick="copyLogToClipboard()" title="Copy activity to clipboard" aria-label="Copy activity to clipboard" style="display:none">
|
|
528
524
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
|
|
529
525
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
|
530
526
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
@@ -532,19 +528,8 @@
|
|
|
532
528
|
</button>
|
|
533
529
|
</div>
|
|
534
530
|
|
|
535
|
-
<!--
|
|
536
|
-
<div class="row info-panel" id="info-panel">
|
|
537
|
-
<div class="info-rows">
|
|
538
|
-
<div class="info-row"><span class="info-row-label">File:</span><span class="info-row-value" id="info-file">—</span></div>
|
|
539
|
-
<div class="info-row"><span class="info-row-label">Page:</span><span class="info-row-value" id="info-page">—</span></div>
|
|
540
|
-
</div>
|
|
541
|
-
</div>
|
|
542
|
-
|
|
543
|
-
<!-- Log panel (shown when Show log active) -->
|
|
531
|
+
<!-- Activity panel (shown when Activity active) -->
|
|
544
532
|
<div class="row log-panel" id="log-panel">
|
|
545
|
-
<div class="log-header">
|
|
546
|
-
<span id="log-servers">0 server(s)</span>
|
|
547
|
-
</div>
|
|
548
533
|
<div class="log-entries" id="log-entries"></div>
|
|
549
534
|
</div>
|
|
550
535
|
</div>
|
|
@@ -561,67 +546,98 @@
|
|
|
561
546
|
|
|
562
547
|
let requestIdCounter = 0;
|
|
563
548
|
|
|
564
|
-
// Status
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
|
|
579
|
-
//
|
|
580
|
-
// NOTE: `activeConnections` is declared inside the connection-pool IIFE
|
|
581
|
-
// below (around line ~617), so it is NOT in scope from this top-level
|
|
582
|
-
// function. The IIFE exposes `window.__wsGetActiveConnections()` for
|
|
583
|
-
// exactly this reason — calling that getter is what makes the pill
|
|
584
|
-
// update once the pool reports a live connection.
|
|
585
|
-
function renderModeText() {
|
|
586
|
-
try {
|
|
587
|
-
var getConns = window.__wsGetActiveConnections;
|
|
588
|
-
var conns = typeof getConns === 'function' ? (getConns() || []) : [];
|
|
589
|
-
var localUp = conns.some(function(c) {
|
|
590
|
-
return c && c.ws && c.ws.readyState === 1 && c.port !== 'cloud';
|
|
591
|
-
});
|
|
592
|
-
var cloudUp = conns.some(function(c) {
|
|
593
|
-
return c && c.ws && c.ws.readyState === 1 && c.port === 'cloud';
|
|
594
|
-
});
|
|
595
|
-
if (localUp && cloudUp) return 'Local + Cloud';
|
|
596
|
-
if (localUp) return 'Local';
|
|
597
|
-
if (cloudUp) return 'Cloud';
|
|
598
|
-
return '';
|
|
599
|
-
} catch (e) {
|
|
600
|
-
return '';
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function refreshStatus() {
|
|
549
|
+
// Status inputs owned by the message handler at the bottom of this file.
|
|
550
|
+
// bridgeError flips true on a code.js ERROR message; a later successful
|
|
551
|
+
// VARIABLES_DATA clears it (the plugin recovered).
|
|
552
|
+
var bridgeError = false;
|
|
553
|
+
|
|
554
|
+
// Single source of truth for the status pill. Derives the rendered state
|
|
555
|
+
// from actual facts instead of trusting one-off events:
|
|
556
|
+
// - live WS connections (local + cloud) from the connection pool
|
|
557
|
+
// - userPaused (pool exposes __wsIsPaused)
|
|
558
|
+
// - scanning/discovery activity (__wsIsScanning / __wsIsDiscoveryActive)
|
|
559
|
+
// - bridgeError (ERROR message from code.js)
|
|
560
|
+
// window.__figmaVariablesReady is also an input (set by VARIABLES_DATA);
|
|
561
|
+
// it no longer drives the pill green on its own — a Figma-side data event
|
|
562
|
+
// says nothing about whether an AI app is actually connected.
|
|
563
|
+
function deriveAndRenderStatus() {
|
|
605
564
|
var dot = document.getElementById('status-dot');
|
|
606
565
|
var stateText = document.getElementById('status-state');
|
|
607
|
-
var labelEl = document.querySelector('.status-text .label');
|
|
608
566
|
if (!dot || !stateText) return;
|
|
609
|
-
|
|
610
|
-
var
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
567
|
+
|
|
568
|
+
var conns = (typeof window.__wsGetActiveConnections === 'function') ? (window.__wsGetActiveConnections() || []) : [];
|
|
569
|
+
var live = 0;
|
|
570
|
+
for (var i = 0; i < conns.length; i++) {
|
|
571
|
+
if (conns[i] && conns[i].ws && conns[i].ws.readyState === 1) live++;
|
|
572
|
+
}
|
|
573
|
+
var paused = (typeof window.__wsIsPaused === 'function') ? window.__wsIsPaused() : false;
|
|
574
|
+
var scanning = (typeof window.__wsIsScanning === 'function' && window.__wsIsScanning()) ||
|
|
575
|
+
(typeof window.__wsIsDiscoveryActive === 'function' && window.__wsIsDiscoveryActive());
|
|
576
|
+
|
|
577
|
+
var dotClass, text;
|
|
578
|
+
if (bridgeError) {
|
|
579
|
+
dotClass = 'error';
|
|
580
|
+
text = 'Something broke';
|
|
581
|
+
} else if (paused) {
|
|
582
|
+
dotClass = 'paused';
|
|
583
|
+
text = 'Paused — AI can\'t make changes';
|
|
584
|
+
} else if (live >= 1) {
|
|
585
|
+
dotClass = 'active';
|
|
586
|
+
text = 'Connected — AI can work in this file';
|
|
587
|
+
} else if (scanning) {
|
|
588
|
+
dotClass = 'loading';
|
|
589
|
+
text = 'Looking for your AI app…';
|
|
590
|
+
} else {
|
|
591
|
+
// Defensive fallback: discovery is always on unless paused, so this
|
|
592
|
+
// only renders if the discovery loop is somehow disabled.
|
|
593
|
+
dotClass = '';
|
|
594
|
+
text = 'Not connected';
|
|
595
|
+
}
|
|
596
|
+
dot.className = 'status-indicator' + (dotClass ? ' ' + dotClass : '');
|
|
597
|
+
stateText.textContent = text;
|
|
598
|
+
stateText.title = text; // full sentence survives ellipsis truncation
|
|
599
|
+
|
|
600
|
+
var hint = document.getElementById('status-error-hint');
|
|
601
|
+
if (hint) hint.style.display = bridgeError ? 'block' : 'none';
|
|
602
|
+
|
|
603
|
+
renderStatusMeta(live);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Small always-visible line under the pill: "Connected to N AI app(s)"
|
|
607
|
+
// plus proof of life ("Last action: Xs ago"). Hidden entirely when there
|
|
608
|
+
// is nothing to show (zero connections).
|
|
609
|
+
function renderStatusMeta(liveCount) {
|
|
610
|
+
var meta = document.getElementById('status-meta');
|
|
611
|
+
var countEl = document.getElementById('status-conn-count');
|
|
612
|
+
var lastEl = document.getElementById('status-last-action');
|
|
613
|
+
if (!meta || !countEl || !lastEl) return;
|
|
614
|
+
var anything = false;
|
|
615
|
+
if (liveCount >= 1) {
|
|
616
|
+
countEl.textContent = 'Connected to ' + liveCount + ' AI app' + (liveCount === 1 ? '' : 's');
|
|
617
|
+
countEl.style.display = '';
|
|
618
|
+
anything = true;
|
|
619
|
+
} else {
|
|
620
|
+
countEl.textContent = '';
|
|
621
|
+
countEl.style.display = 'none';
|
|
622
|
+
}
|
|
623
|
+
if (liveCount >= 1 && _lastActionAt) {
|
|
624
|
+
lastEl.textContent = 'Last action: ' + formatAgo(Date.now() - _lastActionAt);
|
|
625
|
+
lastEl.style.display = '';
|
|
626
|
+
anything = true;
|
|
627
|
+
} else {
|
|
628
|
+
lastEl.textContent = '';
|
|
629
|
+
lastEl.style.display = 'none';
|
|
630
|
+
}
|
|
631
|
+
meta.style.display = anything ? 'flex' : 'none';
|
|
616
632
|
}
|
|
617
633
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
634
|
+
function formatAgo(ms) {
|
|
635
|
+
var s = Math.max(0, Math.floor(ms / 1000));
|
|
636
|
+
if (s < 60) return s + 's ago';
|
|
637
|
+
var m = Math.floor(s / 60);
|
|
638
|
+
if (m < 60) return m + 'm ago';
|
|
639
|
+
var h = Math.floor(m / 60);
|
|
640
|
+
return h + 'h ago';
|
|
625
641
|
}
|
|
626
642
|
|
|
627
643
|
// ============================================================================
|
|
@@ -1004,11 +1020,7 @@
|
|
|
1004
1020
|
|
|
1005
1021
|
// Multi-connection state: plugin connects to ALL active MCP servers
|
|
1006
1022
|
// so that every Claude tab/CLI instance gets Figma access.
|
|
1007
|
-
var activeConnections = []; // Array of { port, ws }
|
|
1008
|
-
var wsReconnectDelay = 500;
|
|
1009
|
-
var wsMaxReconnectDelay = 5000;
|
|
1010
|
-
var wsReconnectAttempts = 0;
|
|
1011
|
-
var wsMaxReconnectAttempts = 50;
|
|
1023
|
+
var activeConnections = []; // Array of { port, ws, serverVersion?, serverInfo? }
|
|
1012
1024
|
var isScanning = false;
|
|
1013
1025
|
|
|
1014
1026
|
// Backward-compat: ws and wsConnected reflect "at least one connection"
|
|
@@ -1031,6 +1043,28 @@
|
|
|
1031
1043
|
'CREATE_VARIABLE_COLLECTION': function(params) { return window.createVariableCollection(params.name, params); },
|
|
1032
1044
|
'GET_LOCAL_COMPONENTS': function() { return window.getLocalComponents(); },
|
|
1033
1045
|
'INSTANTIATE_COMPONENT': function(params) { return window.instantiateComponent(params.componentKey, params); },
|
|
1046
|
+
// Component set creation — forward params wholesale; result payload rides in msg.data.
|
|
1047
|
+
// Timeout scales with variant count (the plugin builds all variants in one
|
|
1048
|
+
// uncancellable pass; a fixed 30s ceiling made big matrices report failure
|
|
1049
|
+
// while the set still got created). Mirrors componentSetTimeoutMs in
|
|
1050
|
+
// websocket-connector.ts, which adds a 5s buffer on the server hop so this
|
|
1051
|
+
// hop's result always arrives first — keep the base formula in sync.
|
|
1052
|
+
'CREATE_COMPONENT_SET': function(params) {
|
|
1053
|
+
var variantCount = 1;
|
|
1054
|
+
if (params && params.componentIds && params.componentIds.length) {
|
|
1055
|
+
variantCount = params.componentIds.length;
|
|
1056
|
+
} else if (params && params.properties) {
|
|
1057
|
+
for (var axis in params.properties) {
|
|
1058
|
+
if (params.properties.hasOwnProperty(axis)) {
|
|
1059
|
+
var vals = params.properties[axis];
|
|
1060
|
+
variantCount *= Math.max(1, (vals && vals.length) || 1);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
var timeout = Math.min(120000, Math.max(30000, variantCount * 1200));
|
|
1065
|
+
return window.sendPluginCommand('CREATE_COMPONENT_SET', params, timeout)
|
|
1066
|
+
.catch(function(err) { return { success: false, error: err.message || String(err) }; });
|
|
1067
|
+
},
|
|
1034
1068
|
'GET_COMPONENT': function(params) { return window.requestComponentData(params.nodeId); },
|
|
1035
1069
|
'SET_NODE_DESCRIPTION': function(params) { return window.setNodeDescription(params.nodeId, params.description, params.descriptionMarkdown); },
|
|
1036
1070
|
'ADD_COMPONENT_PROPERTY': function(params) { return window.addComponentProperty(params.nodeId, params.propertyName, params.propertyType, params.defaultValue, params); },
|
|
@@ -1187,6 +1221,9 @@
|
|
|
1187
1221
|
function removeConnection(port) {
|
|
1188
1222
|
activeConnections = activeConnections.filter(function(c) { return c.port !== port; });
|
|
1189
1223
|
updateCompatState();
|
|
1224
|
+
// Re-pace the health discovery loop: dropping to zero connections
|
|
1225
|
+
// switches it to the fast (3s) cadence immediately.
|
|
1226
|
+
scheduleDiscovery();
|
|
1190
1227
|
}
|
|
1191
1228
|
|
|
1192
1229
|
/**
|
|
@@ -1204,9 +1241,9 @@
|
|
|
1204
1241
|
ws = null;
|
|
1205
1242
|
wsPort = null;
|
|
1206
1243
|
}
|
|
1207
|
-
//
|
|
1208
|
-
// Guarded for the unlikely case it's invoked before
|
|
1209
|
-
if (typeof
|
|
1244
|
+
// deriveAndRenderStatus is defined in the outer scope (same script tag,
|
|
1245
|
+
// hoisted). Guarded for the unlikely case it's invoked before then.
|
|
1246
|
+
if (typeof deriveAndRenderStatus === 'function') deriveAndRenderStatus();
|
|
1210
1247
|
}
|
|
1211
1248
|
|
|
1212
1249
|
/**
|
|
@@ -1241,14 +1278,9 @@
|
|
|
1241
1278
|
// After connecting, disconnect-triggered retries have their own limit.
|
|
1242
1279
|
var initialScanAttempts = 0;
|
|
1243
1280
|
var MAX_INITIAL_SCANS = 3;
|
|
1244
|
-
// Set true only when the user clicks Pause. Suppresses the
|
|
1245
|
-
//
|
|
1281
|
+
// Set true only when the user clicks Pause. Suppresses the health
|
|
1282
|
+
// discovery loop so a deliberate pause is not undone automatically.
|
|
1246
1283
|
var userPaused = false;
|
|
1247
|
-
// Watchdog cadence: how often to re-probe for an MCP server while we have
|
|
1248
|
-
// ZERO connections. Runs only during genuine downtime and stops the moment
|
|
1249
|
-
// a server connects, so the unavoidable connection-refused console noise is
|
|
1250
|
-
// bounded to periods when nothing is connected anyway.
|
|
1251
|
-
var BACKGROUND_RESCAN_MS = 12000;
|
|
1252
1284
|
|
|
1253
1285
|
function wsScanAndConnect() {
|
|
1254
1286
|
if (isScanning) return;
|
|
@@ -1285,8 +1317,6 @@
|
|
|
1285
1317
|
pending--;
|
|
1286
1318
|
if (pending <= 0) {
|
|
1287
1319
|
isScanning = false;
|
|
1288
|
-
wsReconnectDelay = 500;
|
|
1289
|
-
wsReconnectAttempts = 0;
|
|
1290
1320
|
}
|
|
1291
1321
|
};
|
|
1292
1322
|
|
|
@@ -1302,11 +1332,11 @@
|
|
|
1302
1332
|
// Retry with backoff if no servers found, up to MAX_INITIAL_SCANS
|
|
1303
1333
|
if (!foundAny && activeConnections.length === 0) {
|
|
1304
1334
|
// Fast burst on load: a couple of quick retries with backoff.
|
|
1305
|
-
// Once the burst exhausts we STOP re-arming here — the
|
|
1306
|
-
//
|
|
1307
|
-
//
|
|
1308
|
-
//
|
|
1309
|
-
//
|
|
1335
|
+
// Once the burst exhausts we STOP re-arming here — the HTTP
|
|
1336
|
+
// /health discovery loop takes over and keeps probing quietly
|
|
1337
|
+
// (fetch to a closed port rejects catchably, no console spam),
|
|
1338
|
+
// so a server that starts AFTER the plugin still connects
|
|
1339
|
+
// without a restart.
|
|
1310
1340
|
if (initialScanAttempts < MAX_INITIAL_SCANS) {
|
|
1311
1341
|
initialScanAttempts++;
|
|
1312
1342
|
if (initialScanAttempts < MAX_INITIAL_SCANS) {
|
|
@@ -1314,7 +1344,7 @@
|
|
|
1314
1344
|
console.log('[MCP Bridge] No servers found, retry ' + initialScanAttempts + '/' + MAX_INITIAL_SCANS + ' in ' + (delay/1000) + 's');
|
|
1315
1345
|
setTimeout(wsScanAndConnect, delay);
|
|
1316
1346
|
} else {
|
|
1317
|
-
console.log('[MCP Bridge] No MCP server yet —
|
|
1347
|
+
console.log('[MCP Bridge] No MCP server yet — health discovery keeps probing every ' + (DISCOVERY_IDLE_MS/1000) + 's until one appears (no restart needed).');
|
|
1318
1348
|
}
|
|
1319
1349
|
}
|
|
1320
1350
|
}
|
|
@@ -1328,10 +1358,18 @@
|
|
|
1328
1358
|
|
|
1329
1359
|
/**
|
|
1330
1360
|
* Reconnect a specific port that disconnected.
|
|
1331
|
-
* Tries the same port
|
|
1332
|
-
*
|
|
1361
|
+
* Tries the same port once (server may have just restarted); anything
|
|
1362
|
+
* slower is picked up by the /health discovery loop.
|
|
1333
1363
|
*/
|
|
1334
1364
|
function wsReconnectPort(port) {
|
|
1365
|
+
if (port === 'cloud') {
|
|
1366
|
+
// The cloud connection is a wss:// relay, not a localhost port —
|
|
1367
|
+
// 'ws://localhost:cloud' is an invalid URL. Re-dial through the
|
|
1368
|
+
// cloud code (correct URL + its own bounded retry/give-up).
|
|
1369
|
+
if (typeof window.__cloudReconnect === 'function') window.__cloudReconnect();
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
if (userPaused || isPortConnected(port)) return;
|
|
1335
1373
|
try {
|
|
1336
1374
|
var testWs = new WebSocket('ws://localhost:' + port);
|
|
1337
1375
|
var timeout = setTimeout(function() {
|
|
@@ -1351,7 +1389,39 @@
|
|
|
1351
1389
|
clearTimeout(timeout);
|
|
1352
1390
|
};
|
|
1353
1391
|
} catch (e) {
|
|
1354
|
-
// Port gone —
|
|
1392
|
+
// Port gone — the discovery loop will find it if it comes back
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Connect directly to a port the /health probe just verified is OUR
|
|
1398
|
+
* server. Same attach path as the scan, plus the server version the
|
|
1399
|
+
* probe reported (stored on the connection for display/diagnostics).
|
|
1400
|
+
*/
|
|
1401
|
+
function wsConnectToPort(port, serverVersion) {
|
|
1402
|
+
if (userPaused || isPortConnected(port)) return;
|
|
1403
|
+
try {
|
|
1404
|
+
var testWs = new WebSocket('ws://localhost:' + port);
|
|
1405
|
+
var timeout = setTimeout(function() {
|
|
1406
|
+
if (testWs.readyState !== 1) testWs.close();
|
|
1407
|
+
}, 3000);
|
|
1408
|
+
|
|
1409
|
+
testWs.onopen = function() {
|
|
1410
|
+
clearTimeout(timeout);
|
|
1411
|
+
var conn = { port: port, ws: testWs };
|
|
1412
|
+
if (serverVersion) conn.serverVersion = serverVersion;
|
|
1413
|
+
activeConnections.push(conn);
|
|
1414
|
+
updateCompatState();
|
|
1415
|
+
console.log('[MCP Bridge] WebSocket connected to port ' + port + (serverVersion ? ' (server v' + serverVersion + ')' : '') + ' (' + activeConnections.length + ' server(s) total)');
|
|
1416
|
+
attachWsHandlers(testWs, port);
|
|
1417
|
+
initializeConnection(testWs, port);
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
testWs.onerror = function() {
|
|
1421
|
+
clearTimeout(timeout);
|
|
1422
|
+
};
|
|
1423
|
+
} catch (e) {
|
|
1424
|
+
// ignore — next discovery pass will retry
|
|
1355
1425
|
}
|
|
1356
1426
|
}
|
|
1357
1427
|
|
|
@@ -1367,7 +1437,16 @@
|
|
|
1367
1437
|
if (message.type === 'SERVER_HELLO' && message.data) {
|
|
1368
1438
|
console.log('[MCP Bridge] Connected to server on port ' + message.data.port + ' (PID: ' + message.data.pid + ', v' + message.data.serverVersion + ')');
|
|
1369
1439
|
var conn = activeConnections.find(function(c) { return c.ws === activeWs; });
|
|
1370
|
-
if (conn)
|
|
1440
|
+
if (conn) {
|
|
1441
|
+
conn.serverInfo = message.data;
|
|
1442
|
+
if (message.data.serverVersion) conn.serverVersion = message.data.serverVersion;
|
|
1443
|
+
}
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// Server says the installed plugin is older than it expects.
|
|
1448
|
+
if (message.type === 'PLUGIN_UPDATE_AVAILABLE') {
|
|
1449
|
+
if (typeof showUpdateBanner === 'function') showUpdateBanner();
|
|
1371
1450
|
return;
|
|
1372
1451
|
}
|
|
1373
1452
|
|
|
@@ -1415,12 +1494,10 @@
|
|
|
1415
1494
|
return;
|
|
1416
1495
|
}
|
|
1417
1496
|
|
|
1418
|
-
//
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
setTimeout(function() { wsReconnectPort(port); }, delay);
|
|
1423
|
-
}
|
|
1497
|
+
// One immediate same-port retry covers fast same-port server
|
|
1498
|
+
// restarts; anything slower is picked up by the /health discovery
|
|
1499
|
+
// loop, so the old capped retry-with-backoff dance is gone.
|
|
1500
|
+
setTimeout(function() { wsReconnectPort(port); }, 1000);
|
|
1424
1501
|
};
|
|
1425
1502
|
|
|
1426
1503
|
activeWs.onerror = function() {
|
|
@@ -1513,8 +1590,7 @@
|
|
|
1513
1590
|
// Reset scan state so a future Resume gets a clean retry budget.
|
|
1514
1591
|
isScanning = false;
|
|
1515
1592
|
initialScanAttempts = 0;
|
|
1516
|
-
|
|
1517
|
-
// Deliberate user pause — keep the watchdog quiet until Resume/Reconnect.
|
|
1593
|
+
// Deliberate user pause — keep discovery quiet until Resume.
|
|
1518
1594
|
userPaused = true;
|
|
1519
1595
|
};
|
|
1520
1596
|
|
|
@@ -1530,20 +1606,92 @@
|
|
|
1530
1606
|
|
|
1531
1607
|
window.__wsIsPaused = function() { return userPaused; };
|
|
1532
1608
|
window.__wsIsScanning = function() { return isScanning; };
|
|
1609
|
+
// The health discovery loop is armed whenever the user hasn't paused —
|
|
1610
|
+
// used by the status pill to show "Looking for your AI app…" honestly.
|
|
1611
|
+
window.__wsIsDiscoveryActive = function() { return !userPaused; };
|
|
1612
|
+
|
|
1613
|
+
// ======================================================================
|
|
1614
|
+
// HTTP /health DISCOVERY LOOP — replaces the old zero-connection-gated
|
|
1615
|
+
// WS watchdog. fetch() to a closed port rejects catchably with NO
|
|
1616
|
+
// console spam (unlike new WebSocket()), so this can run always-on:
|
|
1617
|
+
// - every 3s while zero connections (fast attach after server start)
|
|
1618
|
+
// - every 10s while connected (catches a restarted instance among
|
|
1619
|
+
// live ones — previously a permanent dead end)
|
|
1620
|
+
// A port only gets a WS dial after its /health response matches OUR
|
|
1621
|
+
// server's payload shape ({status:'ok', version, clients, ...}).
|
|
1622
|
+
// ======================================================================
|
|
1623
|
+
var HEALTH_TIMEOUT_MS = 1500;
|
|
1624
|
+
var DISCOVERY_IDLE_MS = 3000; // cadence with zero connections
|
|
1625
|
+
var DISCOVERY_STEADY_MS = 10000; // cadence with >=1 connection
|
|
1626
|
+
var discoveryTimer = null;
|
|
1627
|
+
var discoveryInFlight = false;
|
|
1628
|
+
|
|
1629
|
+
function liveConnectionCount() {
|
|
1630
|
+
var n = 0;
|
|
1631
|
+
for (var i = 0; i < activeConnections.length; i++) {
|
|
1632
|
+
if (activeConnections[i].ws.readyState === 1) n++;
|
|
1633
|
+
}
|
|
1634
|
+
return n;
|
|
1635
|
+
}
|
|
1533
1636
|
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
setInterval(function() {
|
|
1539
|
-
if (userPaused || isScanning) return;
|
|
1540
|
-
if (activeConnections.length > 0) return;
|
|
1541
|
-
wsScanAndConnect();
|
|
1542
|
-
}, BACKGROUND_RESCAN_MS);
|
|
1637
|
+
function isOurHealthPayload(json) {
|
|
1638
|
+
// Shape served by websocket-server.ts handleHttpRequest()
|
|
1639
|
+
return !!(json && json.status === 'ok' && typeof json.version === 'string' && json.clients !== undefined);
|
|
1640
|
+
}
|
|
1543
1641
|
|
|
1544
|
-
|
|
1545
|
-
|
|
1642
|
+
function probePortHealth(port) {
|
|
1643
|
+
var opts = {};
|
|
1644
|
+
try {
|
|
1645
|
+
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
1646
|
+
opts.signal = AbortSignal.timeout(HEALTH_TIMEOUT_MS);
|
|
1647
|
+
}
|
|
1648
|
+
} catch (e) { /* AbortSignal unavailable — fetch just runs untimed */ }
|
|
1649
|
+
return fetch('http://localhost:' + port + '/health', opts)
|
|
1650
|
+
.then(function(res) {
|
|
1651
|
+
if (!res.ok) return null;
|
|
1652
|
+
return res.json().then(function(json) {
|
|
1653
|
+
return isOurHealthPayload(json) ? { port: port, version: json.version } : null;
|
|
1654
|
+
});
|
|
1655
|
+
})
|
|
1656
|
+
.catch(function() { return null; }); // closed port / timeout / non-JSON — all quiet
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
function scheduleDiscovery() {
|
|
1660
|
+
if (discoveryTimer) clearTimeout(discoveryTimer);
|
|
1661
|
+
var delay = liveConnectionCount() > 0 ? DISCOVERY_STEADY_MS : DISCOVERY_IDLE_MS;
|
|
1662
|
+
discoveryTimer = setTimeout(runDiscoveryPass, delay);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function runDiscoveryPass() {
|
|
1666
|
+
if (discoveryInFlight) { scheduleDiscovery(); return; }
|
|
1667
|
+
if (userPaused) { scheduleDiscovery(); return; } // never auto-connect while paused
|
|
1668
|
+
var candidates = [];
|
|
1669
|
+
for (var p = WS_PORT_RANGE_START; p <= WS_PORT_RANGE_END; p++) {
|
|
1670
|
+
if (!isPortConnected(p)) candidates.push(p);
|
|
1671
|
+
}
|
|
1672
|
+
if (candidates.length === 0) { scheduleDiscovery(); return; }
|
|
1673
|
+
discoveryInFlight = true;
|
|
1674
|
+
Promise.all(candidates.map(probePortHealth))
|
|
1675
|
+
.then(function(results) {
|
|
1676
|
+
for (var i = 0; i < results.length; i++) {
|
|
1677
|
+
var hit = results[i];
|
|
1678
|
+
if (hit && !userPaused && !isPortConnected(hit.port)) {
|
|
1679
|
+
wsConnectToPort(hit.port, hit.version);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
})
|
|
1683
|
+
.catch(function() { /* individual probes already swallow errors */ })
|
|
1684
|
+
.then(function() {
|
|
1685
|
+
discoveryInFlight = false;
|
|
1686
|
+
scheduleDiscovery();
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// Initial scan on load (fast WS burst for instant attach). The discovery
|
|
1691
|
+
// loop then keeps probing forever: fast while disconnected, slow while
|
|
1692
|
+
// connected. Disconnect-triggered same-port retries handle quick drops.
|
|
1546
1693
|
wsScanAndConnect();
|
|
1694
|
+
scheduleDiscovery();
|
|
1547
1695
|
})();
|
|
1548
1696
|
|
|
1549
1697
|
// ============================================================================
|
|
@@ -1551,6 +1699,14 @@
|
|
|
1551
1699
|
// ============================================================================
|
|
1552
1700
|
var cloudWs = null;
|
|
1553
1701
|
var CLOUD_RELAY_HOST = 'wss://figma-console-mcp.southleft.com';
|
|
1702
|
+
var _lastCloudCode = null; // most recent code that successfully paired
|
|
1703
|
+
var _cloudUserDisconnected = false; // user clicked Disconnect — suppress auto re-dial
|
|
1704
|
+
var _cloudWasConnectedBeforePause = false; // restore cloud on Resume after Pause
|
|
1705
|
+
var _cloudRetryCount = 0; // bounded auto-reconnect budget
|
|
1706
|
+
var CLOUD_MAX_RETRIES = 5;
|
|
1707
|
+
var _cloudEverConnected = false; // paired successfully THIS session — separates a real
|
|
1708
|
+
// drop (worth telling the user) from a stale restored
|
|
1709
|
+
// code that never connected (stay quiet)
|
|
1554
1710
|
|
|
1555
1711
|
// ============================================================================
|
|
1556
1712
|
// 3-STAGE UI TOGGLES
|
|
@@ -1645,10 +1801,10 @@
|
|
|
1645
1801
|
var opening = !document.getElementById('cloud-pair').classList.contains('visible');
|
|
1646
1802
|
if (opening) closeSubToolbarIfOpen();
|
|
1647
1803
|
toggleRow('cloud-pair', 'cloud-icon');
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1804
|
+
// Re-derive on both open and close: opening shows the current cloud
|
|
1805
|
+
// state (previously it could open onto a stale blank), closing hides
|
|
1806
|
+
// routine states while keeping actionable ones ("pairing lost") visible.
|
|
1807
|
+
renderCloudStatus();
|
|
1652
1808
|
}
|
|
1653
1809
|
|
|
1654
1810
|
function toggleCloudHelp() {
|
|
@@ -1661,48 +1817,21 @@
|
|
|
1661
1817
|
var nowOpen = toggleRow('sub-toolbar', 'expand-btn');
|
|
1662
1818
|
document.getElementById('expand-btn').textContent = nowOpen ? '−' : '+';
|
|
1663
1819
|
if (!nowOpen) {
|
|
1664
|
-
// Collapse
|
|
1665
|
-
var info = document.getElementById('info-panel');
|
|
1820
|
+
// Collapse the Activity panel if sub-toolbar closes
|
|
1666
1821
|
var log = document.getElementById('log-panel');
|
|
1667
|
-
if (info.classList.contains('visible')) toggleInfo();
|
|
1668
1822
|
if (log.classList.contains('visible')) toggleLog();
|
|
1669
1823
|
}
|
|
1670
1824
|
}
|
|
1671
1825
|
|
|
1672
|
-
function toggleInfo() {
|
|
1673
|
-
var opening = !document.getElementById('info-panel').classList.contains('visible');
|
|
1674
|
-
if (opening && document.getElementById('log-panel').classList.contains('visible')) {
|
|
1675
|
-
// Close log first so only one panel is open at a time.
|
|
1676
|
-
toggleLog();
|
|
1677
|
-
}
|
|
1678
|
-
toggleRow('info-panel', 'info-toggle');
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
1826
|
function toggleLog() {
|
|
1682
1827
|
var logVisible = document.getElementById('log-panel').classList.contains('visible');
|
|
1683
1828
|
var opening = !logVisible;
|
|
1684
|
-
if (opening && document.getElementById('info-panel').classList.contains('visible')) {
|
|
1685
|
-
// Close info first so only one panel is open at a time.
|
|
1686
|
-
toggleRow('info-panel', 'info-toggle');
|
|
1687
|
-
}
|
|
1688
1829
|
toggleRow('log-panel', 'log-toggle');
|
|
1689
|
-
// Reveal
|
|
1690
|
-
document.getElementById('errors-toggle').style.display = opening ? '' : 'none';
|
|
1830
|
+
// Reveal the Copy button only when the Activity panel is open
|
|
1691
1831
|
document.getElementById('copy-log-btn').style.display = opening ? '' : 'none';
|
|
1692
1832
|
autoResize();
|
|
1693
1833
|
}
|
|
1694
1834
|
|
|
1695
|
-
function toggleErrorsOnly() {
|
|
1696
|
-
var btn = document.getElementById('errors-toggle');
|
|
1697
|
-
var entries = document.getElementById('log-entries');
|
|
1698
|
-
var on = !btn.classList.contains('active');
|
|
1699
|
-
btn.classList.toggle('active', on);
|
|
1700
|
-
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
1701
|
-
entries.classList.toggle('errors-only', on);
|
|
1702
|
-
btn.setAttribute('title', on ? 'Remove error filter' : 'Filter errors only');
|
|
1703
|
-
btn.setAttribute('aria-label', on ? 'Remove error filter' : 'Filter errors only');
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
1835
|
// Follow Figma's theme. With themeColors:true, Figma adds a "figma-light" /
|
|
1707
1836
|
// "figma-dark" class to <html> and updates it live when the user switches
|
|
1708
1837
|
// themes. We mirror that onto body[data-theme] so the status/log color
|
|
@@ -1735,6 +1864,9 @@
|
|
|
1735
1864
|
var logHistory = [];
|
|
1736
1865
|
var logEntriesEl = null;
|
|
1737
1866
|
var _ctaBtn = null;
|
|
1867
|
+
// Timestamp of the most recent command sent through the bridge (the same
|
|
1868
|
+
// event that appends to logHistory) — drives "Last action: Xs ago".
|
|
1869
|
+
var _lastActionAt = null;
|
|
1738
1870
|
|
|
1739
1871
|
function ensureLogRefs() {
|
|
1740
1872
|
if (!logEntriesEl) logEntriesEl = document.getElementById('log-entries');
|
|
@@ -1809,7 +1941,7 @@
|
|
|
1809
1941
|
try { ok = document.execCommand('copy'); } catch (e) {}
|
|
1810
1942
|
document.body.removeChild(ta);
|
|
1811
1943
|
if (ok) {
|
|
1812
|
-
logWithHistory('Copied to
|
|
1944
|
+
logWithHistory('Copied to clipboard (' + logHistory.length + ' entries)', 'success');
|
|
1813
1945
|
} else {
|
|
1814
1946
|
logWithHistory('Copy failed - clipboard not available', 'error');
|
|
1815
1947
|
}
|
|
@@ -1901,6 +2033,7 @@
|
|
|
1901
2033
|
var durEntry = null;
|
|
1902
2034
|
var histIdx = -1;
|
|
1903
2035
|
if (summary) {
|
|
2036
|
+
_lastActionAt = Date.now();
|
|
1904
2037
|
logWithHistory(summary, 'info');
|
|
1905
2038
|
histIdx = logHistory.length - 1;
|
|
1906
2039
|
ensureLogRefs();
|
|
@@ -1942,40 +2075,42 @@
|
|
|
1942
2075
|
);
|
|
1943
2076
|
};
|
|
1944
2077
|
|
|
1945
|
-
//
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
PLUGIN_VERSION = 'v' + info.pluginVersion;
|
|
1957
|
-
}
|
|
1958
|
-
}).catch(function() { /* ignore */ });
|
|
2078
|
+
// Plugin update banner — shown when the server pushes
|
|
2079
|
+
// PLUGIN_UPDATE_AVAILABLE over the WebSocket. Dismiss sticks for the
|
|
2080
|
+
// session so repeated pushes don't nag.
|
|
2081
|
+
var _updateBannerDismissed = false;
|
|
2082
|
+
function showUpdateBanner() {
|
|
2083
|
+
if (_updateBannerDismissed) return;
|
|
2084
|
+
var b = document.getElementById('update-banner');
|
|
2085
|
+
if (b && !b.classList.contains('visible')) {
|
|
2086
|
+
b.classList.add('visible');
|
|
2087
|
+
autoResize();
|
|
2088
|
+
}
|
|
1959
2089
|
}
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2090
|
+
function dismissUpdateBanner() {
|
|
2091
|
+
_updateBannerDismissed = true;
|
|
2092
|
+
var b = document.getElementById('update-banner');
|
|
2093
|
+
if (b && b.classList.contains('visible')) {
|
|
2094
|
+
b.classList.remove('visible');
|
|
2095
|
+
autoResize();
|
|
1966
2096
|
}
|
|
1967
|
-
var el = document.getElementById('log-servers');
|
|
1968
|
-
if (el) el.textContent = n + ' server(s)';
|
|
1969
2097
|
}
|
|
1970
2098
|
|
|
1971
|
-
//
|
|
2099
|
+
// True while a cloud relay connection is live in the shared pool.
|
|
2100
|
+
function hasLiveCloudConnection() {
|
|
2101
|
+
var conns = (typeof window.__wsGetActiveConnections === 'function') ? (window.__wsGetActiveConnections() || []) : [];
|
|
2102
|
+
return conns.some(function(c) { return c && c.port === 'cloud' && c.ws && c.ws.readyState === 1; });
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// Real Pause / Resume: toggles the local WebSocket bridge.
|
|
1972
2106
|
function setCtaState(state) {
|
|
1973
2107
|
ensureLogRefs();
|
|
1974
2108
|
if (!_ctaBtn) return;
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
else if (state === '
|
|
1978
|
-
else if (state === '
|
|
2109
|
+
_ctaBtn.style.display = ''; // markup starts it hidden until first honest state
|
|
2110
|
+
if (state === 'on') { _ctaBtn.textContent = 'Pause'; _ctaBtn.disabled = false; }
|
|
2111
|
+
else if (state === 'paused') { _ctaBtn.textContent = 'Resume'; _ctaBtn.disabled = false; }
|
|
2112
|
+
else if (state === 'reconnect'){ _ctaBtn.textContent = 'Try again'; _ctaBtn.disabled = false; }
|
|
2113
|
+
else if (state === 'scanning') { _ctaBtn.textContent = 'Connecting…'; _ctaBtn.disabled = true; }
|
|
1979
2114
|
}
|
|
1980
2115
|
|
|
1981
2116
|
function toggleLocalConnection() {
|
|
@@ -1983,16 +2118,27 @@
|
|
|
1983
2118
|
if (!_ctaBtn) return;
|
|
1984
2119
|
var label = _ctaBtn.textContent;
|
|
1985
2120
|
if (label === 'Pause') {
|
|
2121
|
+
// Remember a live cloud connection so Resume can restore it.
|
|
2122
|
+
_cloudWasConnectedBeforePause = hasLiveCloudConnection();
|
|
1986
2123
|
if (typeof window.__wsDisconnectAll === 'function') window.__wsDisconnectAll();
|
|
1987
2124
|
setCtaState('paused');
|
|
1988
|
-
|
|
2125
|
+
deriveAndRenderStatus();
|
|
1989
2126
|
logWithHistory('Paused', 'warn');
|
|
1990
|
-
} else if (label === 'Resume' || label === '
|
|
2127
|
+
} else if (label === 'Resume' || label === 'Try again') {
|
|
1991
2128
|
setCtaState('scanning');
|
|
1992
|
-
|
|
1993
|
-
logWithHistory((label === 'Reconnect' ? 'Reconnecting' : 'Resuming') + ', scanning ports 9223-9232', 'info');
|
|
2129
|
+
logWithHistory('Looking for your AI app…', 'info');
|
|
1994
2130
|
if (typeof window.__wsManualScan === 'function') window.__wsManualScan();
|
|
1995
2131
|
else if (typeof window.__wsScanAndConnect === 'function') window.__wsScanAndConnect();
|
|
2132
|
+
// Re-establish the cloud connection if one existed before Pause
|
|
2133
|
+
// (the scan above only covers localhost ports).
|
|
2134
|
+
if (_cloudWasConnectedBeforePause) {
|
|
2135
|
+
_cloudWasConnectedBeforePause = false;
|
|
2136
|
+
if (_lastCloudCode && !_cloudUserDisconnected) {
|
|
2137
|
+
_cloudRetryCount = 0;
|
|
2138
|
+
cloudDial(_lastCloudCode, true);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
deriveAndRenderStatus();
|
|
1996
2142
|
// Watch for connection success or scan timeout.
|
|
1997
2143
|
var attempts = 0;
|
|
1998
2144
|
var poller = setInterval(function() {
|
|
@@ -2001,26 +2147,25 @@
|
|
|
2001
2147
|
if (n > 0) {
|
|
2002
2148
|
clearInterval(poller);
|
|
2003
2149
|
setCtaState('on');
|
|
2004
|
-
|
|
2150
|
+
deriveAndRenderStatus();
|
|
2005
2151
|
} else if (attempts > 20) { // ~10s max
|
|
2006
2152
|
clearInterval(poller);
|
|
2007
|
-
setCtaState('
|
|
2008
|
-
|
|
2009
|
-
logWithHistory('
|
|
2153
|
+
setCtaState('reconnect');
|
|
2154
|
+
deriveAndRenderStatus();
|
|
2155
|
+
logWithHistory('Couldn\'t find your AI app. Open Claude (or your AI assistant), then press Try again.', 'error');
|
|
2010
2156
|
}
|
|
2011
2157
|
}, 500);
|
|
2012
2158
|
}
|
|
2013
2159
|
}
|
|
2014
2160
|
|
|
2015
|
-
// Keep the CTA button honest about the real connection state
|
|
2016
|
-
//
|
|
2017
|
-
//
|
|
2018
|
-
//
|
|
2019
|
-
// of a misleading "Pause". Skips while a scan is mid-flight (button disabled
|
|
2020
|
-
// or __wsIsScanning) to avoid fighting the transient state set on click.
|
|
2161
|
+
// Keep the CTA button honest about the real connection state: a
|
|
2162
|
+
// never-connected or dropped plugin shows a clickable "Try again" instead
|
|
2163
|
+
// of a misleading "Pause". Skips while a scan is mid-flight (button
|
|
2164
|
+
// disabled or __wsIsScanning) to avoid fighting the transient click state.
|
|
2021
2165
|
function reconcileCta() {
|
|
2022
2166
|
ensureLogRefs();
|
|
2023
|
-
if (!_ctaBtn
|
|
2167
|
+
if (!_ctaBtn) return;
|
|
2168
|
+
if (_ctaBtn.disabled) return;
|
|
2024
2169
|
if (typeof window.__wsIsScanning === 'function' && window.__wsIsScanning()) return;
|
|
2025
2170
|
var n = (typeof window.__wsGetActiveConnections === 'function') ? window.__wsGetActiveConnections().length : 0;
|
|
2026
2171
|
var paused = (typeof window.__wsIsPaused === 'function') ? window.__wsIsPaused() : false;
|
|
@@ -2029,32 +2174,76 @@
|
|
|
2029
2174
|
} else if (n > 0) {
|
|
2030
2175
|
if (_ctaBtn.textContent !== 'Pause') setCtaState('on');
|
|
2031
2176
|
} else {
|
|
2032
|
-
if (_ctaBtn.textContent !== '
|
|
2177
|
+
if (_ctaBtn.textContent !== 'Try again') setCtaState('reconnect');
|
|
2033
2178
|
}
|
|
2179
|
+
// First reconcile also unhides the button with its honest label.
|
|
2180
|
+
if (_ctaBtn.style.display === 'none') _ctaBtn.style.display = '';
|
|
2034
2181
|
}
|
|
2035
2182
|
|
|
2036
|
-
// Periodic
|
|
2183
|
+
// Periodic reconcile of CTA + status pill/meta (cheap, <1ms). Also
|
|
2184
|
+
// refreshes "Last action: Xs ago" via renderStatusMeta inside
|
|
2185
|
+
// deriveAndRenderStatus.
|
|
2037
2186
|
setInterval(function() {
|
|
2038
|
-
updateServerCount();
|
|
2039
2187
|
reconcileCta();
|
|
2040
|
-
|
|
2041
|
-
var info = document.getElementById('info-panel');
|
|
2042
|
-
if (info && info.classList.contains('visible')) updateInfoPanel();
|
|
2188
|
+
deriveAndRenderStatus();
|
|
2043
2189
|
}, 2000);
|
|
2044
2190
|
|
|
2045
|
-
// One-shot on first ready
|
|
2046
|
-
|
|
2191
|
+
// One-shot on first ready: render honest initial state and capture the
|
|
2192
|
+
// plugin's own version (used in the Activity export header) without
|
|
2193
|
+
// logging a command line — hence the unwrapped sender. The version is
|
|
2194
|
+
// read from the GET_FILE_INFO_RESULT handler below.
|
|
2195
|
+
setTimeout(function() {
|
|
2196
|
+
reconcileCta();
|
|
2197
|
+
deriveAndRenderStatus();
|
|
2198
|
+
_origSendPluginCommand('GET_FILE_INFO', {}).catch(function() { /* ignore */ });
|
|
2199
|
+
}, 500);
|
|
2047
2200
|
|
|
2048
2201
|
// Size the plugin window to fit content on first paint.
|
|
2049
2202
|
requestAnimationFrame(autoResize);
|
|
2050
2203
|
|
|
2051
|
-
|
|
2204
|
+
// Single source of truth for the cloud pairing status line — derives the
|
|
2205
|
+
// text from actual state (same philosophy as deriveAndRenderStatus for the
|
|
2206
|
+
// pill) instead of trusting one-off events. Every string is prefixed
|
|
2207
|
+
// "Cloud" so it can never read as the plugin's overall status, and the
|
|
2208
|
+
// line stays empty (hidden via :not(:empty)) unless it has something
|
|
2209
|
+
// useful to say — a background failure like an expired pairing code
|
|
2210
|
+
// restored from a previous session stays silent. Transient messages
|
|
2211
|
+
// ("Cloud: connecting…", bad-code errors) are written directly by
|
|
2212
|
+
// cloudDial/cloudConnect and survive until the next state change.
|
|
2213
|
+
function renderCloudStatus() {
|
|
2052
2214
|
var statusEl = document.getElementById('cloud-status');
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2215
|
+
if (!statusEl) return;
|
|
2216
|
+
var cp = document.getElementById('cloud-pair');
|
|
2217
|
+
var panelOpen = !!(cp && cp.classList.contains('visible'));
|
|
2218
|
+
var paused = (typeof window.__wsIsPaused === 'function') ? window.__wsIsPaused() : false;
|
|
2219
|
+
|
|
2220
|
+
if (paused) {
|
|
2221
|
+
// Pause closes the cloud socket deliberately and Resume redials it —
|
|
2222
|
+
// never present that as a lost pairing (retry is suppressed while
|
|
2223
|
+
// paused, so "reconnecting…" would also be untrue).
|
|
2224
|
+
statusEl.textContent = panelOpen ? 'Cloud: paused' : '';
|
|
2225
|
+
statusEl.className = 'cloud-status';
|
|
2226
|
+
} else if (hasLiveCloudConnection()) {
|
|
2227
|
+
statusEl.textContent = panelOpen ? 'Cloud: connected' : '';
|
|
2228
|
+
statusEl.className = 'cloud-status connected';
|
|
2229
|
+
} else if (!_cloudUserDisconnected && _cloudEverConnected && _cloudRetryCount < CLOUD_MAX_RETRIES) {
|
|
2230
|
+
// Paired earlier this session and dropped — actionable, so it shows
|
|
2231
|
+
// even while the panel is collapsed.
|
|
2232
|
+
statusEl.textContent = 'Cloud pairing lost — reconnecting…';
|
|
2233
|
+
statusEl.className = 'cloud-status';
|
|
2234
|
+
} else if (!_cloudUserDisconnected && _cloudEverConnected) {
|
|
2235
|
+
statusEl.textContent = 'Cloud pairing lost. Generate a fresh code in Claude and reconnect.';
|
|
2236
|
+
statusEl.className = 'cloud-status error';
|
|
2237
|
+
} else {
|
|
2238
|
+
statusEl.textContent = panelOpen ? 'Cloud: not connected' : '';
|
|
2056
2239
|
statusEl.className = 'cloud-status';
|
|
2057
2240
|
}
|
|
2241
|
+
autoResize();
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
function resetCloudUI() {
|
|
2245
|
+
var btn = document.getElementById('cloud-btn');
|
|
2246
|
+
renderCloudStatus();
|
|
2058
2247
|
if (btn) {
|
|
2059
2248
|
btn.disabled = false;
|
|
2060
2249
|
btn.textContent = 'Connect';
|
|
@@ -2063,11 +2252,12 @@
|
|
|
2063
2252
|
cloudWs = null;
|
|
2064
2253
|
}
|
|
2065
2254
|
|
|
2066
|
-
|
|
2067
|
-
|
|
2255
|
+
// Shared dialer used by manual Connect, auto-reconnect after a drop,
|
|
2256
|
+
// restored-config auto-connect, and Resume-after-Pause. `isAuto` softens
|
|
2257
|
+
// the error UX for background attempts and arms the bounded retry chain.
|
|
2258
|
+
function cloudDial(code, isAuto) {
|
|
2068
2259
|
var btn = document.getElementById('cloud-btn');
|
|
2069
2260
|
var statusEl = document.getElementById('cloud-status');
|
|
2070
|
-
var code = (codeInput.value || '').trim().toUpperCase();
|
|
2071
2261
|
|
|
2072
2262
|
if (!code || code.length < 6) {
|
|
2073
2263
|
statusEl.textContent = 'Pairing code must be 6 characters';
|
|
@@ -2075,24 +2265,35 @@
|
|
|
2075
2265
|
return;
|
|
2076
2266
|
}
|
|
2077
2267
|
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
statusEl.className = 'cloud-status';
|
|
2081
|
-
|
|
2082
|
-
// Close existing cloud connection if any
|
|
2268
|
+
// Close existing cloud connection if any (manual reason suppresses
|
|
2269
|
+
// the pool's same-port retry for the old socket).
|
|
2083
2270
|
if (cloudWs && cloudWs.readyState <= 1) {
|
|
2084
|
-
cloudWs.close();
|
|
2271
|
+
try { cloudWs.close(1000, 'Manual disconnect'); } catch (e) {}
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
if (btn) btn.disabled = true;
|
|
2275
|
+
// Manual attempts show progress; auto re-dials render the derived state
|
|
2276
|
+
// ("Cloud pairing lost — reconnecting…", or silence for a background
|
|
2277
|
+
// restore) so an unlabeled transient never floats under the main pill.
|
|
2278
|
+
if (isAuto) {
|
|
2279
|
+
renderCloudStatus();
|
|
2280
|
+
} else if (statusEl) {
|
|
2281
|
+
statusEl.textContent = 'Cloud: connecting…';
|
|
2282
|
+
statusEl.className = 'cloud-status';
|
|
2085
2283
|
}
|
|
2086
2284
|
|
|
2087
2285
|
try {
|
|
2286
|
+
var opened = false;
|
|
2287
|
+
var sawError = false;
|
|
2088
2288
|
cloudWs = new WebSocket(CLOUD_RELAY_HOST + '/ws/pair?code=' + code);
|
|
2089
2289
|
|
|
2090
2290
|
cloudWs.onopen = function() {
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2291
|
+
opened = true;
|
|
2292
|
+
_lastCloudCode = code;
|
|
2293
|
+
_cloudUserDisconnected = false;
|
|
2294
|
+
_cloudRetryCount = 0;
|
|
2295
|
+
_cloudEverConnected = true;
|
|
2296
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Disconnect'; btn.onclick = cloudDisconnect; }
|
|
2096
2297
|
|
|
2097
2298
|
// Add to the shared connection pool (uses same handlers as localhost).
|
|
2098
2299
|
// Pass resetCloudUI as disconnect callback — attachWsHandlers overwrites
|
|
@@ -2101,6 +2302,9 @@
|
|
|
2101
2302
|
window.__wsAddCloudConnection(cloudWs, 'cloud', resetCloudUI);
|
|
2102
2303
|
}
|
|
2103
2304
|
|
|
2305
|
+
// After the pool add, so hasLiveCloudConnection() sees this socket.
|
|
2306
|
+
renderCloudStatus();
|
|
2307
|
+
|
|
2104
2308
|
// Persist cloud config via code.js clientStorage
|
|
2105
2309
|
parent.postMessage({ pluginMessage: {
|
|
2106
2310
|
type: 'STORE_CLOUD_CONFIG',
|
|
@@ -2109,27 +2313,76 @@
|
|
|
2109
2313
|
};
|
|
2110
2314
|
|
|
2111
2315
|
cloudWs.onerror = function() {
|
|
2112
|
-
|
|
2113
|
-
statusEl
|
|
2114
|
-
|
|
2316
|
+
sawError = true;
|
|
2317
|
+
if (statusEl && !isAuto) {
|
|
2318
|
+
statusEl.textContent = 'That code didn\'t work. Ask Claude for a fresh pairing code and try again.';
|
|
2319
|
+
statusEl.className = 'cloud-status error';
|
|
2320
|
+
}
|
|
2321
|
+
if (btn) btn.disabled = false;
|
|
2115
2322
|
};
|
|
2116
2323
|
|
|
2117
2324
|
// Note: onclose here handles pre-connection close (e.g., bad code).
|
|
2118
2325
|
// After onopen, attachWsHandlers overwrites this — resetCloudUI callback
|
|
2119
2326
|
// handles post-connection close instead.
|
|
2120
2327
|
cloudWs.onclose = function(event) {
|
|
2121
|
-
|
|
2328
|
+
if (!opened && sawError && !isAuto) {
|
|
2329
|
+
// Keep the visible error message (onclose fires right after
|
|
2330
|
+
// onerror and would otherwise clobber it); restore the button.
|
|
2331
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Connect'; btn.onclick = cloudConnect; }
|
|
2332
|
+
cloudWs = null;
|
|
2333
|
+
} else {
|
|
2334
|
+
resetCloudUI();
|
|
2335
|
+
}
|
|
2336
|
+
// Auto attempt that never opened → keep trying within the budget.
|
|
2337
|
+
if (isAuto && !opened) scheduleCloudRetry();
|
|
2122
2338
|
};
|
|
2123
2339
|
} catch (e) {
|
|
2124
|
-
statusEl.textContent = 'Failed: ' + e.message;
|
|
2340
|
+
if (statusEl) { statusEl.textContent = 'Failed: ' + e.message; statusEl.className = 'cloud-status error'; }
|
|
2341
|
+
if (btn) btn.disabled = false;
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// Bounded auto-reconnect with backoff. Gives up after CLOUD_MAX_RETRIES,
|
|
2346
|
+
// never fires while paused or after a deliberate user Disconnect.
|
|
2347
|
+
function scheduleCloudRetry() {
|
|
2348
|
+
if (_cloudUserDisconnected || !_lastCloudCode) return;
|
|
2349
|
+
if (typeof window.__wsIsPaused === 'function' && window.__wsIsPaused()) return;
|
|
2350
|
+
if (_cloudRetryCount >= CLOUD_MAX_RETRIES) return;
|
|
2351
|
+
_cloudRetryCount++;
|
|
2352
|
+
setTimeout(function() {
|
|
2353
|
+
if (_cloudUserDisconnected || hasLiveCloudConnection()) return;
|
|
2354
|
+
if (typeof window.__wsIsPaused === 'function' && window.__wsIsPaused()) return;
|
|
2355
|
+
cloudDial(_lastCloudCode, true);
|
|
2356
|
+
}, Math.min(2000 * _cloudRetryCount, 10000));
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// Called by the connection pool when the pooled cloud connection drops —
|
|
2360
|
+
// re-dials the actual relay URL instead of 'ws://localhost:cloud'.
|
|
2361
|
+
window.__cloudReconnect = function() {
|
|
2362
|
+
scheduleCloudRetry();
|
|
2363
|
+
};
|
|
2364
|
+
|
|
2365
|
+
function cloudConnect() {
|
|
2366
|
+
var codeInput = document.getElementById('cloud-code');
|
|
2367
|
+
var statusEl = document.getElementById('cloud-status');
|
|
2368
|
+
var code = (codeInput.value || '').trim().toUpperCase();
|
|
2369
|
+
|
|
2370
|
+
if (!code || code.length < 4) {
|
|
2371
|
+
statusEl.textContent = 'Enter pairing code';
|
|
2125
2372
|
statusEl.className = 'cloud-status error';
|
|
2126
|
-
|
|
2373
|
+
return;
|
|
2127
2374
|
}
|
|
2375
|
+
|
|
2376
|
+
_cloudUserDisconnected = false;
|
|
2377
|
+
_cloudRetryCount = 0;
|
|
2378
|
+
cloudDial(code, false);
|
|
2128
2379
|
}
|
|
2129
2380
|
|
|
2130
2381
|
function cloudDisconnect() {
|
|
2382
|
+
_cloudUserDisconnected = true; // deliberate — suppress auto re-dial
|
|
2383
|
+
_cloudEverConnected = false; // a deliberate disconnect is not a "lost" pairing
|
|
2131
2384
|
if (cloudWs) {
|
|
2132
|
-
cloudWs.close();
|
|
2385
|
+
try { cloudWs.close(1000, 'Manual disconnect'); } catch (e) {}
|
|
2133
2386
|
}
|
|
2134
2387
|
// Reset UI immediately — don't rely on onclose (may be overwritten)
|
|
2135
2388
|
resetCloudUI();
|
|
@@ -2154,6 +2407,8 @@
|
|
|
2154
2407
|
if (msg.oldName !== undefined) result.oldName = msg.oldName;
|
|
2155
2408
|
if (msg.instance !== undefined) result.instance = msg.instance;
|
|
2156
2409
|
if (msg.warnings !== undefined) result.warnings = msg.warnings;
|
|
2410
|
+
if (msg.updatedCount !== undefined) result.updatedCount = msg.updatedCount;
|
|
2411
|
+
if (msg.nodes !== undefined) result.nodes = msg.nodes;
|
|
2157
2412
|
request.resolve(result);
|
|
2158
2413
|
} else {
|
|
2159
2414
|
request.resolve({ success: false, error: msg.error || 'Unknown error' });
|
|
@@ -2167,7 +2422,10 @@
|
|
|
2167
2422
|
case 'VARIABLES_DATA':
|
|
2168
2423
|
window.__figmaVariablesData = msg.data;
|
|
2169
2424
|
window.__figmaVariablesReady = true;
|
|
2170
|
-
|
|
2425
|
+
// A Figma-side data event says nothing about the WS connection —
|
|
2426
|
+
// record the fact and let the derived state decide the pill.
|
|
2427
|
+
bridgeError = false;
|
|
2428
|
+
deriveAndRenderStatus();
|
|
2171
2429
|
console.log('[MCP Bridge] Active - ' + (msg.data.variables?.length || 0) + ' vars');
|
|
2172
2430
|
// Forward to WebSocket client if connected
|
|
2173
2431
|
if (window.__wsForwardVariables) window.__wsForwardVariables(msg.data);
|
|
@@ -2186,7 +2444,8 @@
|
|
|
2186
2444
|
|
|
2187
2445
|
case 'ERROR':
|
|
2188
2446
|
window.__figmaVariablesReady = false;
|
|
2189
|
-
|
|
2447
|
+
bridgeError = true; // pill shows "Something broke" + reopen hint
|
|
2448
|
+
deriveAndRenderStatus();
|
|
2190
2449
|
console.error('[MCP Bridge] Error:', msg.error);
|
|
2191
2450
|
break;
|
|
2192
2451
|
|
|
@@ -2230,6 +2489,11 @@
|
|
|
2230
2489
|
case 'INSTANTIATE_COMPONENT_RESULT':
|
|
2231
2490
|
handleResult('INSTANTIATE_COMPONENT', 'instance');
|
|
2232
2491
|
break;
|
|
2492
|
+
// Component set creation (payload flows via msg.data — already on the
|
|
2493
|
+
// handleResult whitelist, so no new top-level fields to forward)
|
|
2494
|
+
case 'CREATE_COMPONENT_SET_RESULT':
|
|
2495
|
+
handleResult('CREATE_COMPONENT_SET', null);
|
|
2496
|
+
break;
|
|
2233
2497
|
|
|
2234
2498
|
// NEW: Component property operations
|
|
2235
2499
|
case 'SET_NODE_DESCRIPTION_RESULT':
|
|
@@ -2381,9 +2645,31 @@
|
|
|
2381
2645
|
|
|
2382
2646
|
// File info
|
|
2383
2647
|
case 'GET_FILE_INFO_RESULT':
|
|
2648
|
+
// Capture the plugin's own version for the Activity export header
|
|
2649
|
+
// (moved here from the deleted Info panel so any GET_FILE_INFO
|
|
2650
|
+
// response keeps it fresh, regardless of who asked).
|
|
2651
|
+
var fileInfo = msg.fileInfo || msg.data;
|
|
2652
|
+
if (fileInfo && fileInfo.pluginVersion) {
|
|
2653
|
+
PLUGIN_VERSION = 'v' + fileInfo.pluginVersion;
|
|
2654
|
+
}
|
|
2384
2655
|
handleResult('GET_FILE_INFO', 'fileInfo');
|
|
2385
2656
|
break;
|
|
2386
2657
|
|
|
2658
|
+
// Cloud config restored from clientStorage by code.js — auto-fill the
|
|
2659
|
+
// pairing input and reconnect without making the user re-pair.
|
|
2660
|
+
case 'CLOUD_CONFIG_RESTORED':
|
|
2661
|
+
if (msg.config && msg.config.code && !_cloudUserDisconnected) {
|
|
2662
|
+
var restoredCode = String(msg.config.code).trim().toUpperCase();
|
|
2663
|
+
var restoredInput = document.getElementById('cloud-code');
|
|
2664
|
+
if (restoredInput) restoredInput.value = restoredCode;
|
|
2665
|
+
if (restoredCode.length >= 4 && !hasLiveCloudConnection()) {
|
|
2666
|
+
_lastCloudCode = restoredCode;
|
|
2667
|
+
_cloudRetryCount = 0;
|
|
2668
|
+
cloudDial(restoredCode, true);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
break;
|
|
2672
|
+
|
|
2387
2673
|
// Plugin UI reload
|
|
2388
2674
|
case 'RELOAD_UI_RESULT':
|
|
2389
2675
|
handleResult('RELOAD_UI', null);
|