- **Installed plugins survive a crash.** Importing, installing, updating or removing a plugin, and a
plugin changing its settings, now sets `Runtime::unsaved`; at the end of each frame the app hands
the preferences to a new `Services::save_prefs`, and the desktop app writes `state.json` at once
(temporary file + rename, the same atomic write the quit path uses, now shared). Nothing waits for
quit any more.
- **The UI never waits on the network.** New `Session::start_background(id, params)`: the command
runs as usual until it makes a server request; the request goes out on the link's own thread
(`LiveLink::background`, implemented by the desktop `NetLink` with one short-lived thread per
request) and the command stops; when the answer arrives (`Event::Net`, taken by `live_poll`, which
the network's waker already triggers) the command runs again from the start on the file it
started on, answered from the answers so far, until it needs no new request. All server requests
go through one funnel (`Session::net`), so `plugin.directory.*`, `library.publishToServer`,
`library.add` (links), `library.usage` and library fetches work this way without being split.
A re-run of `library.publishToServer` caches the library it actually sent (a fresh copy differs
in its time and, on a first publish, its id). One job per command; `cancel_background` drops a
late answer.
- **UI:** `FigApp::run_background(ctx, id, params, done)` with a boxed completion. Manage plugins >
Browse shows "Fetching from the server..." with Cancel instead of Browse; the install confirmation
shows "Installing..." with Cancel and stays open until the download is in (an error leaves it open
to retry); row buttons wait during an install; closing the window cancels. Assets > Libraries:
Publish to server shows "Publishing..." + Cancel, Show usage and pasting a team library link show
"Fetching from the server..." + Cancel. No polling repaints: the window draws again when the
answer's waker fires; pending state is a scan of a short Vec, no per-frame allocation.
- **Programmatic callers are unchanged:** MCP, CLI, control channel and scripts still run the same
commands synchronously (and the browser link, which has no background requests, too).
- New string "Installing..." (en/es); docs: plugins.md (immediate saves, Browse/Install never wait),
server.md (team library requests never wait).
Widgets for FigCraft's own WebAssembly plugins (plugin API 3; API 1 and 2 plugins run unchanged), plus the panel tail.
- **Widgets.** A manifest with `"api": 3, "widget": true` makes a widget plugin. Plugins > Widgets (or `widget.insert {plugin, x?, y?}`) puts its widget on the page: the plugin is called with `widget: {id, state, event}` and hands back a description through the new `widget_render` import (`{state?, root}`: frames with auto layout, text, rectangles, ellipses and images it carries as base64). FigCraft validates the description (4 MB, 256 layers, 16 deep, finite numbers within ranges, hex colours, text length and control characters, a 16 KB state, images 1 MB each / 2 MB together / 4096 px a side) and builds it as a frame whose layers are locked, so the widget moves as one layer.
- **Clicks.** A plain click with the Move tool on a part the plugin named as a `target` (the cursor turns into a pointing hand there; quick repeated clicks each count) calls the plugin back in a fresh, metered sandbox (same fuel, memory and call limits as runs; nothing kept between calls, so the state and the click alone decide the result) as one undo step, and the widget is redrawn. Dragging moves it; Shift/Cmd/Alt clicks only select. Agents use `widget.click {id, target}` and `widget.list`.
- **State in the document.** The plugin id and the state live in the widget's own layer data (`figcraft.widget*`), so widgets survive save and reload, duplicate/copy and paste (the copy is a widget of its own), undo/redo, export (the built layers draw) and live sessions (synced like any layer data). No plugin can write `figcraft.widget*` data or call `widget.*`; a callback can only rebuild its own widget, and touches other layers only through the commands its manifest permits. A widget whose plugin is missing stays as drawn.
- **Example.** `plugins/examples/counter`: a counter between - and + buttons (read access, no assets). The SDK gained `Input::widget` and `render_widget`.
- **Panel tail.** Panels of several plugins are open at once, side by side (`plugin.ui` without an id returns the one opened last); a colour field's swatch opens FigCraft's colour picker beside the plugin's window (the plugin hears of the colour when a drag ends; also for parameters forms). Swatch grid's palette panel gained a label colour field.
- Docs: plugins.md (ABI v3, widget description, commands, limits), mcp.md, target-app-parity area 18, ROADMAP, gaps gap 6, roadmap M11; Spanish labels for the new commands.
Plugin ABI v2: a plugin can show a panel (text, number, checkbox, select, colour, list, notes, buttons) rendered with FigCraft's widgets, react to events with its state kept between calls (each editing callback one undo step, full sandbox limits on every call), and store per-plugin settings (64 KB). plugin.ui / plugin.event / plugin.close drive panels from the CLI and MCP; v1 plugins run unchanged. The Swatch grid example gains a Palettes panel that remembers the last palette.