Repository navigation
Releases: denysosadchyi/figmosha2
Release list
Figmosha 2.3.0
Changed
- The plugin bar names its file. Once connected it shows the Figma file's
name instead of "connected", cut with an ellipsis if long. - The plugin bar counts open files. With two or more plugin windows
connected, the check icon sits in a white pill with this window's position,
e.g.1/2 ✓. A single file shows just the icon. The bridge pushes apeers
message ({index, total}) to every plugin when one connects or leaves; older
plugin builds ignore it. - The plugin bar shows what the file is doing. The check spins while an
exec runs on that file (held at least 400ms so quick ones still show), and a
failed exec turns the bar red with a!for 2s. - New plugin bar layout. A Figma file icon and the name on the left, the
status icon or1/2 ✓pill on the right.
Fixed
- A caller hanging up mid-run let the next caller into the file beside its
still-running script. The hang-up path skipped the interlock when the
reply future was done — but cancellingwait_forcancels that future, so
it always was. Rare on Python 3.12, constant on 3.14: found by the final
Linux run of the scenario fuzzer (34–39 of 300 scenarios failed). - The timeout interlock was per connection, not per document, so a caller
routed through another tab of the same file got past it. It now covers the
document, and/clearthrough any tab clears it. - A hang-up while the script was being sent could release the lock with
the script already on its way; the send is now always completed and the
run interlocked. h.ck()could not stop a tight loop. It waited for anabortmessage
from the bridge, but a loop that only awaits Figma APIs never lets the plugin
read its messages. The exec's timeout now travels with the script and
h.ck()checks the plugin's own clock: verified live, such a loop stops at
its 1 s timeout instead of running on and holding the file.- A finished script could leave its file interlocked until
/clear. When
its reply landed right at the timeout — whichh.ck()now makes the normal
case — the bridge dropped the reply and then marked the run abandoned.
Both orderings of that race are handled and tested. - A timed-out script's
print()lifted the interlock while it was still
running, letting the next caller write alongside it. Only its final result
or error lifts it now. - A file could stay interlocked forever after a caller hung up. If a
caller disconnected in the same moment its script's reply arrived, the
bridge marked the finished script as still running and blocked the file
until someone POSTed/clear. Found by the scenario fuzzer (seed 24). start-bridge.shfailed on a stock Mac. It required tmux (not shipped
with macOS) and./venv/bin/python, and gave up after 1 s. It now falls back
to a plain background process without tmux, topython3without a venv,
waits up to 10 s, checksaiohttpup front, and has--stop. The docs say
python3/./venv/bin/pythonon macOS, which has nopythoncommand.- Every CLI call took ~2 s on Windows. The CLI talked to
localhost, which
Windows resolves to::1first; the bridge only listened on127.0.0.1, so
each request waited for the IPv6 attempt to fail. The bridge now also listens
on::1(any client sayinglocalhostis fast) and the CLI defaults to
127.0.0.1. A CLI call went from 2157 ms to ~100 ms, an HTTP call to ~2 ms. - Big results were shipped twice. The plugin sent every result as both
textandvalue, and serialized objects three times on the way. It now
serializes once and sends the value only; the bridge derives theresult
text (compact JSON past 1 MB, where indenting alone cost ~120 ms). A 50k-item
result: 641 → 255 ms; a 5 MB string: 495 → 250 ms. The HTTP API is unchanged. print()sent one message per line through two hops; 20k lines took
~0.5 s. Lines are batched now (every 200 lines or 100 ms): 492 → 37 ms.- Two new "Untitled" files were treated as one document, so every write
went to the newest and the other file was unreachable. The plugin identified a
document by its page ids, and every new file starts with page0:1. It now
stores a random id in the file's plugin data once and reports that. - One file open in two tabs had two locks, one per connection, so two
writers could get into the same document at once. The lock is now per
document. - A file could stay locked until the bridge restarted. When a caller hung
up while waiting in the queue, the bridge stopped waiting for the lock, but
the lock request itself lived on, was granted later and never released —
found by the new chaos test. - Malformed
/execand/clearbodies crashed the handler with a 500: a
JSON array instead of an object, a non-stringtarget, a timeout that isn't
a number. They are now a400naming the field; timeouts must be in
(0, 3600] seconds. - A caller that gave up while queued still ran later. The CLI's socket
timeout ignored time spent waiting for a busy file; when it hung up, the
bridge still ran the script once the file was free — a write nobody was
waiting for, likely retried by the caller. The CLI now allows for the queue,
and the bridge drops callers that left. - Descenders in the plugin bar's text were clipped at the bottom.
start-bridge.ps1did not run in Windows PowerShell 5.1, the one Windows
ships with. The file had em dashes and arrows but no BOM, so 5.1 read it as
ANSI and failed with "The string is missing the terminator". It is now pure
ASCII.- The bridge did not start from a folder with a space in its path
(can't open file 'F:\\00'). PS 5.1'sStart-Processdoesn't quote
-ArgumentListarray elements; the arguments are now one pre-quoted string. start-bridge.ps1found the running bridge by greppingnetstatfor
LISTENING, which is translated on non-English Windows, so-Stopand the
already-running check silently did nothing there. It now asks
Get-NetTCPConnection.start-bridge.ps1treats the Microsoft Storepython.exestub as "no
Python" instead of launching it.- The bridge forces UTF-8 on its output, so a Cyrillic, CJK or emoji file name
can't raiseUnicodeEncodeErrorwhen the log is redirected on Windows. - On Windows every closed Figma tab left a
ConnectionResetError [WinError 10054]traceback inbridge.err.log. It was asyncio noise after the
disconnect was already handled, and the bridge now drops it. CLAUDE.mdtold Claude to start the bridge with bash and tmux and described
one maintainer's Mac. It now covers macOS / Linux / WSL and native Windows,
with machine specifics left toCLAUDE.local.md.h.ck()never fired. The bridge sentabortfor a timed-out run, but
the plugin UI dropped every message type exceptexecandping, so the
sandbox never heard about it. The UI now forwardsabort.figmosha doctorwith several files connected and no-Ttold you to
re-run the plugin; it now says to pick a file with-T, and to
figmosha clearwhen the file is interlocked by a timed-out script.--rawandstatusprinted non-Latin text asП…escapes.- The
1/2pill counted connections that had not identified themselves yet,
so it could flash1/3while a tab reconnected. start-bridge.ps1exited 0 when the bridge failed to start and showed the
stdout log, while the reason is on stderr. It now prints the tail of
bridge.err.log, suggests installing the requirements whenaiohttpis
missing, and exits 1.- README's Windows venv line used
&&, which Windows PowerShell 5.1 rejects;
helper and subcommand counts were out of date. - The bridge test fixture reset globals that no longer exist instead of the
multi-file registries. New tests cover the timeout interlock (504 → 409 →
lifts when the orphan replies,abortsent) and the1/2counter. tests/helpers.test.jscrashed before running a single check:code.js
now readsfigma.root.nameon load and the test's Figma stub had noroot.
Added
- Fuzz and hostile-input tests.
tests/test_fuzz.pyruns random
scenarios (tabs opening and closing, agents writing, giving up, hanging up,
failing) and checks after each that no document ran two scripts at once and
nothing leaked;FIGMOSHA_FUZZ_SEEDS=300for a long run. Plus junk from a
misbehaving plugin, duplicate and late results, forged Host / Origin
headers, and 3000 execs that must leave no residue. The state reset moved
totests/conftest.py, so it covers every test file. - Stress and chaos tests.
tests/test_stress.pyruns 50 concurrent
writers through a read-modify-write race (no lost update allowed), 4 files x
15 agents, a chaos mix of impatient, failing and hung-up callers, a plugin
dying mid-queue, a reconnect storm over one document, junk input and 5 MB
payloads.tests/live_stress.pyruns the same race against your real files. - A queue per file for several agents.
/execwaits its turn on the
document's lock, and now says so:targets//statusshow who is running on
each file and who is waiting, callers name themselves with--agent/
FIGMOSHA_AGENT/"agent", replies carryqueued_ms, and
--queue-timeout/"queue_timeout"caps the wait with a503 file busy
that guarantees nothing ran. -Ttakes a document id, shown bytargetsand stored in the file so it
survives reconnects, so two different files with the same name (two fresh
"Untitled" files) can each be targeted. A connection id works too.- Releases, and an Update button when one is out. Figmosha now has a
VERSION(inbridge.py, shown by/statusanddoctor) and is released as
vX.Y.Ztags. Every 6 hours the bridge reads the release tags on GitHub; when
a newer one exists the plugin bar turns blue with "New version X.Y.Z" and an
Update b...
Figmosha 2.2.0
Added
- Multiple Figma files at once. The bridge keeps one connection per open
file running the plugin instead of one globally. The plugin reports its
identity and/execroutes by atarget(CLI-T/--target), resolved as
exact name →fileKey→ unambiguous substring.GET /targetsand
figmosha targetslist what's connected. One file connected with no target
behaves exactly as before, so existing scripts don't change. - Per-file exec lock. A file's plugin sandbox is a single-threaded async
handler over one document and one undo stack, so two concurrent scripts
interleave at everyawaitand invalidate each other'sfindAllsnapshots.
/execnow takes a per-connection lock; callers on different files never wait
on each other. Read-only scripts can passparallel: true(--parallel) to
bypass it and fan out. - Abandoned-run interlock. A script that outlives its
timeoutcan't be
killed, and the next caller used to mutate the file underneath it. The504
now carries awarningand the request id, further execs on that file return
409until the orphan replies, andPOST /clear(figmosha clear -T <file>)
lifts it.force: truepushes past. - Cooperative cancellation.
h.ck()throws once the bridge has abandoned
the run, so a chunked sweep stops instead of mutating under the next caller.
h.aborted()is the non-throwing form. Older plugin builds ignore the signal. - The same document open in several tabs or windows. Each view registers its
own connection and-Troutes to the newest live one;/statusflags the
extra views withsameDocAs.
Fixed
- One file open in two tabs looped forever. Any second
hellofor a known
file name evicted the incumbent, so each side kicked the other out and the
loser reconnected 2s later and kicked back. A live connection is now never
evicted:helloreplaces an older same-file registration only when its socket
is already closed or it fails a 1s liveness ping. - Two different files sharing a name are still refused, and now for the
right reason. The plugin sends adocSig— a hash of its page node ids, since
figma.fileKeyis null for a local dev plugin andfigma.root.idis"0:0"
everywhere — so the bridge distinguishes two views of one document from two
documents, instead of treating both as ambiguous. - Connections registered unnamed, so
-Tcould not find them. The plugin
UI's identity handler wrote to a#serverelement that no longer exists; it
threw beforesendHello()ran. - The bridge log stayed empty.
start-bridge.shpipedbridge.pyinto
teewithout-u, so Python block-buffered stdout and plugin
connect/disconnect events were invisible exactly when they were needed. - In-flight requests routed to a dead connection fail fast instead of hanging,
and a dead connection drops its abandoned interlock with it.
Removed
- The single-slot
1008rejection and itsSlot busyUI state, with the 15s
backoff. The bridge no longer turns away a second connection for a file, and a
tab that becomes visible again reconnects immediately.
v2.1.0 — origin guard, selection awareness, self-diagnosis
The bridge now refuses requests that come from a web page — the one change here worth acting on today.
It executes arbitrary JS inside your open Figma file, so every tab in your browser was part of the threat model: a page could post to localhost:8787 as a text/plain "simple request", dodge the CORS preflight, and silently edit or delete your work. Requests carrying an Origin header are now rejected, and Host is pinned to the loopback names actually served, which closes DNS rebinding. curl and the CLI are unaffected — they never send Origin.
Also in this release
Knows what you selected. h.sel() and figmosha sel close the gap between "this frame here", which you point at with a mouse, and a node id. page and sel now work anywhere an id is taken — figmosha tree sel --layout.
Diagnoses itself. figmosha doctor walks bridge → plugin → round trip → which file is open, and names the fix at whichever link is broken.
Colours and frames without ceremony. h.hex("#1a2b3c") retires the hand-rolled /255. h.frame() applies auto-layout in the order Figma requires — an order that fails silently when you get it wrong, which is why it is now a helper rather than a third paragraph of documentation.
Runs on Windows properly. start-bridge.ps1 launches detached, with -Restart and -Stop. And the CLI no longer crashes on any layer name outside cp1252, which on a default Windows console meant most non-English names.
Reconnects when it should. A half-open socket used to lock a returning plugin out for ~20s. The bridge now pings the incumbent: silence for a second and the newcomer takes over, while a plugin that is genuinely alive keeps the slot — so two Figma windows no longer evict each other forever.
Tested without Figma. A fake plugin drives the real WebSocket, covering the guard, exec round trip, timeouts, disconnect cleanup and slot handover; the pure helpers run against a stubbed Figma.
Upgrading
Pull, then re-run the plugin in Figma — a running plugin keeps the code it started with, so the new helpers will not exist until you do. manifest.json is unchanged, so a re-import is not needed.
Full detail in CHANGELOG.md.