Comments become the task queue for an attached or headless MCP client: comment.prompt returns a bounded Markdown brief, the Comments panel gains Copy for AI, and the MCP server exposes fix_comments, watch_comments, figcraft://comments and wait_for_changes. Mutating comment.* commands append to a bounded in-memory event log; the control channel's events.poll long-polls it without the UI thread, and a successful batch commits its events only if the whole batch lands. No model, network dependency, asset or file-format change.
file.placeImage runs image.place's code and file.exportSvg file.export's,
so their docs now list the names those read (width, height, target, mode;
scale or size and the export options). MCP's create_node passes points,
closed and select to node.create, and pointer and key read ctrl, so their
schemas list them too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`file.open` on a path already open in a tab switched to it and returned
its index, even when another process had changed the file since, so the
caller kept looking at the old copy with nothing saying so (#997). The
tab's disk stamp from #1330 (`DocState::on_disk`) now tells:
- the result says `stale: true` when the file on disk differs from what
the tab read or last saved;
- `reload: true` reads the file again into the tab, as Revert to saved
does (refused while the tab has unsaved changes or is in a live
session), and the result says `reloaded: true`.
MCP's `open_file` takes `reload` and passes it on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`run_command {"command": "inspect.code", "args": {...}}` ran the command
without its params, on the selection. MCP tool arguments are now checked
against each tool's input schema (and the objects it describes inside,
such as a batch's steps; open objects like `params` take anything), and
a step key other than `command`, `params` and `as` is refused in
command.batch, figcraft-cli scripts and the MCP batch tool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
command.batch and the MCP batch tool take onError: "abort" (the default, unchanged) or
"continue". In continue mode each step runs inside a rollback-only checkpoint split out of
Session::grouped: a failed step is rolled back on its own, later steps still run, and the result
is one {step, command, ok, result|error} object per step. What was applied is one undo step; a
reference to a failed step fails too; malformed batches are still rejected before any step runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
node.find {root?, name?, type?, text?, component?, limit?} returns {ids, total} for the strict
descendants of root (or the current page) that match every filter given: name, text and main
component name as case-insensitive substrings, type as an exact type name. It changes neither the
selection, the page nor the undo history. Part of #996; fields/outline are left for later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
The attached backend read each reply from the app's control port with read_line, which has no
limit. It now reads up to 512 MiB before the newline (an app window capture is at most 256 MiB as
base64) and stops at the first byte past that. An oversized or non-UTF-8 reply drops the
connection and is reported like a lost reply; the next call reconnects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
The command parameter docs (CLI `commands`, MCP list_commands, the Actions
palette) and the MCP open_file tool description listed .fig but not
.sketch; docs/mcp.md and docs/cli.md already did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the Figma forum request "Set overlay position relative to element,
not to page": an open overlay action takes anchor: screen (default) |
triggerInside | triggerOutside. With a trigger anchor the position preset
is measured on the layer that opens the overlay instead of the screen.
Outside puts top presets above it and bottom presets below it with the
edges lined up (a dropdown is Bottom left + Outside); Center centres it
on that layer. Manual is unchanged.
The interaction editor gets Relative to (Whole screen / Trigger layer)
and Inside / Outside. The player, the command (and so MCP and the
control channel) carry it; the command's params doc lists it. Figma has
no such setting, so .fig export writes it as a manual offset. Old files
and JSON without anchor read as screen and write unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: resolve SonarCloud maintainability issues
- Define SHA256_FMT constant in packaging script to remove duplicated string literal (shelldre:S1192)
- Remove redundant 'static lifetime annotations from const items (rust:S8863)
- Replace closures with method and function pointers (rust:S1612)
- Extract helper functions in synthetic document generation, bezier polyline flattening, and component restoration to reduce cognitive complexity (rust:S3776)
* fix(packaging): use verify_sha256 helper function to avoid shellcheck SC2059
* fix(engine): remove duplicate closing brace in large_document
* fix(engine): take &mut Document in restore_id_map; keep 'static on associated consts
restore_id_map allocates ids, so it needs a mutable document. Elided
lifetimes in associated consts trip elided_lifetimes_in_associated_constant,
so those annotations are restored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Assign positional parameters to locals in sha256 verification
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
* Dev profile: line tables for workspace crates, no debug info for dependencies
* Tests: one integration-test binary per crate with several test files
* xtask ci: nextest, doctests, tests without examples, --keep-going; add xtask test
A batch step can name itself with "as" and later steps refer to it with
{"$ref": "name"}, alongside positional {"$step": n}. "field" now takes a
slash path into the result as well as a key, so {"$ref": "copy",
"field": "ids/0"} reaches the first id edit.duplicate returned (the
convention variable.bind uses for fills/0/color; a leading "/" as in
JSON Pointer also works, and a key holding "/" still matches first).
Resolution moves to crates/engine/src/batch.rs (BatchStep, BatchScope),
shared by command.batch, the CLI script runner, the control channel and
the MCP batch tool, which now passes "as" through. Duplicate names fail
before any step runs; unknown, same-step or later names, bad paths and
out-of-range indexes fail the batch with the step, name and full path.
$ref is reserved like $step; nested batches keep their own names.
Fixes#994.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
inspect_document sends a shallower tree when the document's is too large
and points at get_node for the rest, so get_node was the way out of that
limit -- and it had none of its own. Measured against a document with a
frame of 15,000 children, through figcraft-cli mcp:
inspect_document depth 64, allPages 14.29 MiB (8.00 MiB cap not
applied before #915)
get_node depth 64 10.51 MiB
Both are over the 10 MiB buffer the report is about, and for get_node no
depth makes it smaller: the width is at depth 1, so depth 1 is already
9.12 MiB of text and depth 0 has no children at all.
get_node now takes the same maxBytes, cuts the subtree by depth as the
document's tree is cut, and then takes a window over the children when
even depth 1 is too large. The marker says how many children were shown
of how many, and the offset to ask for next; `offset` is a new argument
for reading the rest. After this:
get_node depth 64 8.00 MiB, children 11427 of
15000 from 0, next 11427
The hint in inspect_document's marker no longer promises that get_node
has no size limit, since that was the hole: it now says a wide node
answers with a window and an offset.
The test pages through a windowed node and asserts every child is
reported exactly once, in order, with every page inside the limit.
Unbounded, the first page is 23,003 bytes against a 7,667-byte limit.
Two things the limit got wrong, both reported by CodeRabbit on #915.
maxBytes was compared with the escaped text block alone, but what goes
on the wire is that block inside a content object, inside the JSON-RPC
result, with the request id and the newline the transport writes: a
block measured at exactly maxBytes left as a larger message than the
caller said it could read. A kilobyte of the caller's figure is now kept
for that wrapping. About ninety bytes of it is fixed and the rest is the
request id, so the figure holds for any id up to roughly 900 bytes --
every id in practice, a number or a UUID. Sizing the real envelope here
would mean handing the transport's request id to every tool, and the
assumption is written down instead, in the schema and at the constant.
A depth: 0 request over the limit returned early, so the one response
that cannot be made any smaller was the one sent without the truncated
marker -- while a bisection that walked down to depth 0 sent the same
tree with it. Depth 0 now takes the same path as the bisection: the tree
goes out, over the limit, with the marker saying so.
Without the first fix the test reports a limit at the exact size of the
tree as no truncation; without the second the depth-0 marker is absent
(left: (None, None), right: (Some(0), Some(0))).
MCP stdio is newline-delimited JSON, so one tool result is one message
and has to fit the client's read buffer whole; there is no streaming of
part of one. The TypeScript SDK's stdio transport defaults to 10 MiB and
hosts rarely raise it, so a document whose tree went past that killed
the server: the client stopped reading, the pipe broke, and every later
call in the session failed.
The tree is now reported at the deepest depth that fits, found by
bisection, with a `truncated` object giving the depth asked for, the
depth used, both sizes, the limit and a pointer to get_node for reading
one subtree deeply. `maxBytes` lets a caller that knows its own buffer
say so; the default is 8 MiB, leaving the envelope 2 MiB of the usual
10.
The size is measured as the transport sends it -- pretty-printed and
then escaped inside the text block, which adds about a tenth (342,136
bytes of tree became 379,954 on the wire in the fixture). A kilobyte is
reserved for the marker itself, which is added after the measurement.
A limit below even the shallowest tree still answers, at depth 0, with
the marker saying so: there is nothing smaller to send, and an error
would leave the caller with nothing at all.
The tool description now says that each level of depth multiplies the
response and points at get_node, which the report notes would have been
enough on its own to avoid the crash.
Fixes#912.
ExportFormat::VectorDrawable ("XML", .xml) and ExportSetting.vd_compat
(bestEffort / exact / ignore, stored only when not the default) wire the
figcraft-vectordrawable writer into file.export, file.exportLayers and
export.add/update: a vdCompat param (junk refused), svgIdAttribute writes
android:name, and the results carry the writer's warnings (per file for
export layers). The Export section lists XML with an "Android Vector
Drawable" tooltip and its id attribute and compatibility options, File >
Export... gets an XML preset, and the toasts say when content was changed
or left out. CLI: --format xml / .xml outputs, --vd-compat, warnings as
note: lines. MCP: format xml returns the XML as text plus its warnings.
A .fig export writes XML settings as SVG with a note.
Docs: file-format-parity (row and revision), ui-parity and roadmap
revisions, architecture (the crate), cli and mcp.
Escape or Enter while the pen draws ends the path and keeps its vector in
edit mode with the pen, so the next path joins the same layer; another
Escape leaves edit mode. Entering edit mode on a layer that already is a
vector no longer records an empty undo step.
- `figcraft-cli mcp --automation-read-root DIR --automation-write-root DIR` (each repeatable, `--flag=DIR` works too). When either is given, every file an agent's tool call reads must lie inside a read root or a write root, and every file it writes inside a write root. Anything else is an ordinary tool error (`automation path rejected: … is outside the folders that may be written: …`); nothing is written and the server keeps serving. Without the flags nothing changes.
- The desktop app takes the same flags (`figcraft --control PORT --automation-read-root DIR …`) and confines every control request and every frame of every window to them, so `figcraft-cli mcp --attach` drives a confined app. `--attach` together with the flags is a usage error, because the app runs the commands and only the app can confine them.
- New engine module `crates/engine/src/file_access.rs`: `Roots` (resolution and comparison), a thread-scoped `confined(roots, f)`, and the file primitives (`read`, `read_capped`, `open`, `metadata`, `read_dir`, `write` (atomic), `create_dir_all`, `exists`, `is_dir`). Every engine file access now goes through it. `crates/engine/clippy.toml` denies `std::fs::*`, `File::open/create`, `OpenOptions::open`, `Path::exists/is_file/is_dir/metadata/read_dir/canonicalize` and `figcraft_format::{write_file_atomic, save_file, load_file}` everywhere else in the engine, so a command added later is confined without a check of its own.
- `ui.screenshot` and `ui.render` return the PNG as base64 `data` when no `path` is given (before, `ui.screenshot` wrote `figcraft-screenshot.png` in the working directory and `ui.render` required a path). The attached MCP backend uses that, so it no longer writes and deletes temporary PNGs, and a confined app needs no exemption for them.
- `initialize`'s `instructions` name the folders the agent may read and write.
- `figcraft-cli` options may be given more than once (`Args::values`); single-valued options keep the last value, as before.
- Docs: `docs/mcp.md` › Confining file access (options, semantics, coverage, resolution, limits), `docs/control-protocol.md` › Confining file access and the optional `path` of `ui.screenshot` / `ui.render`, `docs/cli.md`, `figcraft-cli --help`, and the Automation line plus a revision row in `docs/target-app-parity.md`.
Fixes#715, fixes#481.
Save as now offers .figcraft and .fig as separate file types. A .fig
destination, chosen in the dialog or passed to file.saveAs and file.save,
writes the existing experimental .fig export of the document that is shown
and reports how much was left out. The file's own path, unsaved changes,
versions, branches and undo history are not touched, and a shown earlier
version stays shown while the copy is written or a Save as choice is
pending.
Plain Save no longer writes over a file whose saved path ends in .fig;
earlier builds could write native data under that name. It asks for a
destination instead.
- Several ids: auto layout keys go to the frame-like layers, the others get the remaining keys
and are named in `warnings`. Refused only when no target can have auto layout.
- fontStyle is never refused: it draws in the nearest face, as a text layer resolves it.
`warnings` says so when that face's weight or slant differs from what the name asks for.
- set_properties passes `warnings` on to the agent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>