* M6: FigJam .jam files open like .fig files (#1342)
A .jam is the same archive as a .fig (its canvas header reads fig-jam.), which the .fig reader already took; File > Open and its filter, drag and drop, file.import and the command line now take .jam too, and the home screen shows its thumbnail. FigJam layers come in as the .fig reader makes them (stickies and shapes with text as frames with their generated layers, other FigJam layers as frames or vectors with a note, unknown types left out with a note). Tests use a synthetic fig-jam. canvas and a .jam made from our own export.
* M12: a FigJam .jam file opens as a board (#1342)
The .fig reader marks a canvas with FigJam's fig-jam. header as FileKind::Board, so a .jam opens with the board toolbar, the dot grid and the board panels and saves as a board; a .fig stays a design file, and importing a .jam into an open file leaves its kind alone. Its objects stay ordinary layers (frames and vectors with the existing import notes). Builds on the .jam opening branch (#1586).
Someone who joins with a comment link (or its join code) sees the file live and adds, replies to, reacts to and resolves comment threads, but can't change the design: their app refuses other edits and the server accepts a comment link's batch only when every change in it touches comment threads. Sharing online makes a comment link and code beside the edit and view ones; the Share popover offers Can edit, Can comment and Can view, and the commenter's toolbar shows a Can comment badge with Move, Hand and Comment.
Backward compatible: clients announce the comments word; one that doesn't is welcomed with a comment link as a viewer and gets comment links listed as view links, a level this build doesn't know reads as view, and an older server that can't make a comment link leaves the share without one.
With pref.scrollWheelZoom on, a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls; Shift+wheel still scrolls sideways, and trackpads, pinches and touch screens are unchanged. A wheel is told from a trackpad by the device macOS reports, elsewhere by line versus pixel steps.
comment.list takes mentions (a name, any case): only threads where a message, the first or a reply, @mentions it, by the same parser the thread view highlights with and each message's mentions reports; combined with the other filters.
wasm32 can't catch a panic, so the command and frame guards do nothing in the browser and a panic ended the page with its unsaved changes. The page now follows its files with unsaved changes after every frame; its panic hook writes them to local storage as .figcraft copies (as many as fit) and says on the page that it stopped. The next page opens them again as unsaved files: file.recover also takes the copy as base64 data. The recovery-copy helpers move from the desktop app into figcraft_engine::branches so both share them.
Each sweep takes at most 250 000 segments and 2 million possible crossings (pairs of segments whose bounds overlap, counted in O(n log n)), and a fold at most 4 million segments swept over its steps; past them the operation is refused with PathOpsError::TooComplex before it runs. A boolean group past the caps draws without its result, with a warning once, and the refusal is cached; vector.flatten refuses it instead of dropping it.
Properties were listed by name, so a rename moved one. Components now keep prop_order (the names as they were made; a rename keeps the place) and every listing follows it. Make slot opens a name field under the frame's Slot row; Enter makes the slot, Escape cancels, a taken name keeps the field open.
* M9: one name for comments and live sessions, shown in the Join dialog, kept in the browser, renamed live (#1380)
* M9: a reconnecting session's reset is its own helper (#1380)
Every message (the first and each reply) gets `mentions`: {name, known,
you} for each @mention, by the rule the thread view highlights with
(figcraft_doc::mentions, now shared with mention_ranges). Names resolve
against the file's people and you; unknown names are kept with
known: false; a@b.com mentions nobody.
* M2: components can't contain their own instances (#1084)
Edits that would make a component hold an instance of itself, directly, through other components or through another variant of its set, are refused with the loop named. Files holding such a loop open with one instance per loop detached and a note; a rebuild never expands an instance inside a copy of itself, so a loop merged in a live session stops growing.
* M2: a variant may hold a sibling variant's instance; only true loops are refused (#1084)
Each variant counts as its own component: refusals and open-time repairs apply only to a component leading back to itself through instances (an instance-swap value included), so files with nested sibling variants open unchanged. sync_instance_depth is split into match_layers and copy_layers.
Found while testing crates/engine/src/cmd/props.rs:614 (also :708, :717, :718, :826 and :1007)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With map: true, edit.duplicate also returns map: each original layer's id, descendants included (nested frames, groups, instances and their sublayers), paired with its copy's id, so a batch can edit inside a copy through a step reference instead of inspecting it. Without map the result is unchanged.
Tools that act on a document, and the control methods engine.execute,
document.inspect, node.get and ui.render, take an optional `document`
(an open file's id from doc.list / document.inspect, or its path). The
call acts on that file and the tab in front stays in front, so an
attached agent keeps editing its own file when the user switches tabs.
Found while testing crates/engine/src/cmd/variables_io.rs:~92 (css_safe, used by css for String values and mode names)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Every command an MCP client runs acts as the client's name from initialize (clientInfo.title, else .name) with ' (agent)' after it, or 'AI agent' without one, carried to the app as engine.execute {agent}. Its messages, reactions and read state are its own, and its messages are marked (agent: true in the file and comment.list, an Agent label in the thread). The user's name and comments made in the app are unchanged; in live sessions the name and marker travel with the thread.
* M1: Crop image from the Object menu, the right-click menu and the image fill row (#1464)
* M1: crop mode shows the image dimmed outside the layer and heavier crop marks (#1464)
* M1: the handle overlay draws gradients and crops in their own helpers (#1464)