diff --git a/crates/engine/src/group_panic_tests.rs b/crates/engine/src/group_panic_tests.rs new file mode 100644 index 00000000..48754e38 --- /dev/null +++ b/crates/engine/src/group_panic_tests.rs @@ -0,0 +1,149 @@ +//! A panic in a grouped closure, outside any command, ends the group like a failure (#1085): the +//! documents, selection, undo and redo, history limit and journal are as they were, and the panic +//! goes on to the caller. + +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::Arc; + +use serde_json::json; + +use crate::{HistoryEntry, NodeId, Session}; + +fn rect(s: &mut Session, x: i64) -> NodeId { + s.execute("shape.rectangle", &json!({"x": x, "y": 0, "width": 10, "height": 10})).unwrap()["id"].as_u64().map(NodeId).unwrap() +} + +fn labels(entries: &[HistoryEntry]) -> Vec { + entries.iter().map(|e| e.label.clone()).collect() +} + +#[test] +fn a_grouped_closure_that_panics_leaves_the_session_as_before() { + let mut s = Session::new(); + let first = rect(&mut s, 0); + let second = rect(&mut s, 20); + s.execute("edit.undo", &json!({})).unwrap(); + s.execute("select.set", &json!({"ids": [first]})).unwrap(); + let ds = s.ds().unwrap(); + let (doc, selection, page) = (ds.doc.clone(), ds.selection.clone(), ds.page); + let (undo, redo, limit) = (labels(&ds.history.undo), labels(&ds.history.redo), ds.history.limit); + assert_eq!(redo.len(), 1); + let journal = s.journal.clone(); + let depth = s.undo_depth(); + + let unwound = catch_unwind(AssertUnwindSafe(|| { + s.grouped("Edit then panic", |s| -> crate::Result<()> { + s.execute("node.set", &json!({"id": first, "props": {"name": "Renamed"}}))?; + rect(s, 40); + s.execute("select.set", &json!({"ids": []}))?; + panic!("a bug in the closure"); + }) + })); + + let payload = unwound.unwrap_err(); + assert_eq!(payload.downcast_ref::<&str>(), Some(&"a bug in the closure"), "the original panic goes on to the caller"); + let ds = s.ds().unwrap(); + assert!(Arc::ptr_eq(&ds.doc, &doc)); + assert_eq!(*ds.doc, *doc); + assert_eq!(ds.selection, selection); + assert_eq!(ds.page, page); + assert_eq!(labels(&ds.history.undo), undo); + assert_eq!(labels(&ds.history.redo), redo); + assert_eq!(ds.history.limit, limit); + assert_eq!(s.journal, journal); + assert_eq!(s.undo_depth(), depth); + assert_eq!(s.group_depth, 0); + assert!(s.closed_in_group.is_empty()); + + // The redo step survived, and the next edit pushes one step of its own. + s.execute("edit.redo", &json!({})).unwrap(); + assert!(s.doc().unwrap().contains(second)); + rect(&mut s, 60); + assert_eq!(s.undo_depth(), (depth.0 + 2, 0)); +} + +#[test] +fn a_grouped_closure_that_panics_releases_the_live_hold_so_later_edits_sync() { + use crate::live_tests::{push, relay, serve, session}; + + let (mut s, script) = session(); + let first = rect(&mut s, 0); + s.execute("collab.host", &json!({})).unwrap(); + let (mut server, id, welcome) = serve(s.doc().unwrap()); + push(&script, vec![welcome]); + s.live_poll(); + assert!(s.live_doc(s.ds().unwrap().uid)); + let before = s.doc().unwrap().clone(); + script.lock().unwrap().msgs.clear(); + + let unwound = catch_unwind(AssertUnwindSafe(|| { + s.grouped("Rename then panic", |s| -> crate::Result<()> { + s.execute("node.set", &json!({"id": first, "props": {"name": "Rolled back"}}))?; + s.live_sync(); + assert!(script.lock().unwrap().msgs.is_empty(), "the edit waits for the group to end"); + panic!("a bug in the live closure"); + }) + })); + + assert!(unwound.is_err()); + assert_eq!(s.doc().unwrap(), &before); + s.live_sync(); + assert!(script.lock().unwrap().msgs.is_empty(), "the rolled-back edit never goes out"); + + s.execute("node.set", &json!({"id": first, "props": {"name": "Sent afterwards"}})).unwrap(); + s.live_sync(); + assert_eq!(script.lock().unwrap().msgs.len(), 1, "the later edit goes out"); + let replies = relay(&script, &mut server, id); + push(&script, replies); + s.live_poll(); + assert_eq!(server.document().node(first).unwrap().name, "Sent afterwards"); + assert_eq!(s.doc().unwrap(), server.document()); +} + +#[test] +fn a_panic_in_an_inner_group_rolls_back_through_the_outer_one() { + let mut s = Session::new(); + let unwound = catch_unwind(AssertUnwindSafe(|| { + s.grouped("outer", |s| { + s.execute("comment.add", &json!({"x": 1, "y": 2, "text": "Rolled back"}))?; + s.grouped("inner", |s| -> crate::Result<()> { + s.execute("comment.add", &json!({"x": 3, "y": 4, "text": "Also rolled back"}))?; + panic!("a bug in the inner closure"); + }) + }) + })); + + assert!(unwound.is_err()); + assert!(s.doc().unwrap().comments.is_empty()); + assert!(s.pending_events.is_empty()); + assert_eq!(s.events.latest(), 0); + assert_eq!(s.group_depth, 0); + assert_eq!(s.undo_depth(), (0, 0)); + + // The session still groups normally afterwards. + s.grouped("again", |s| s.execute("comment.add", &json!({"x": 5, "y": 6, "text": "Kept"})).map(|_| ())).unwrap(); + assert_eq!(s.doc().unwrap().comments.len(), 1); + assert_eq!(s.undo_depth(), (1, 0)); +} + +#[test] +fn a_group_that_closes_and_opens_tabs_then_panics_brings_the_tabs_back() { + let mut s = Session::new(); + s.execute("file.new", &json!({"name": "Second"})).unwrap(); + let uids: Vec<_> = s.docs.iter().map(|ds| ds.uid).collect(); + let active = s.active; + + let unwound = catch_unwind(AssertUnwindSafe(|| { + s.grouped("Close, open, panic", |s| -> crate::Result<()> { + s.execute("file.close", &json!({"index": 1, "force": true}))?; + s.execute("file.new", &json!({"name": "Temporary"}))?; + panic!("a bug after switching tabs"); + }) + })); + + assert!(unwound.is_err()); + assert_eq!(s.docs.iter().map(|ds| ds.uid).collect::>(), uids); + assert_eq!(s.active, active); + assert!(s.closed_in_group.is_empty()); + assert!(s.docs.iter().all(|ds| ds.history.limit != usize::MAX)); +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index e5b924fc..6e570f54 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -327,6 +327,8 @@ mod gesture_preview_tests; #[cfg(test)] mod gradient_shorthand_tests; #[cfg(test)] +mod group_panic_tests; +#[cfg(test)] mod guide_snap_tests; #[cfg(test)] mod hide_publishing_tests; diff --git a/crates/engine/src/session/undo.rs b/crates/engine/src/session/undo.rs index a10b3f4b..4aa84760 100644 --- a/crates/engine/src/session/undo.rs +++ b/crates/engine/src/session/undo.rs @@ -251,7 +251,8 @@ impl Session { } /// Run `f` as one undo step called `label`: the commands it executes push no steps of their - /// own, and when it fails the documents stay at their group-start state. + /// own, and when it fails or panics the documents stay at their group-start state (a panic + /// then carries on unwinding). pub fn grouped(&mut self, label: &str, f: impl FnOnce(&mut Self) -> Result) -> Result { self.ds()?; self.checkpointed(Some(label), f) @@ -261,7 +262,12 @@ impl Session { /// A checkpoint also works between closing the last tab and opening another one. fn checkpointed(&mut self, label: Option<&str>, f: impl FnOnce(&mut Self) -> Result) -> Result { let before = self.group_checkpoint(label.is_some()); - let result = f(self); + // A panic in `f` ends the group like a failure, then goes on to the caller's guard (#1085); + // the stand-in error only drives the rollback and is never returned. + let (result, panic) = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(self))) { + Ok(result) => (result, None), + Err(payload) => (Err(EngineError::Other("the group panicked".into())), Some(payload)), + }; self.group_depth -= 1; let at_root = self.group_depth == 0; for ds in &before.docs { @@ -297,6 +303,9 @@ impl Session { self.closed_in_group.clear(); self.flush_pending_events(); } + if let Some(payload) = panic { + std::panic::resume_unwind(payload); + } result } diff --git a/docs/architecture.md b/docs/architecture.md index 9faed547..5c7300f8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -75,7 +75,8 @@ content. Exports: PNG, JPG, WebP, SVG, PDF, code. There is no `.fig` reader yet Workspace lints deny `unwrap`, `expect`, `panic!` and friends in shipped code; `unsafe_code` is forbidden. `figcraft_engine::guard` turns a panicking command into an error and rolls the document -back; the desktop app also guards each UI frame. The browser build can't catch a panic (wasm32 aborts), so there a panic stops the page and its panic hook keeps the files with unsaved changes in local storage for the next page. Junk-parameter sweeps run every command with bad +back, and a panic in a `Session::grouped` closure outside any command rolls the group back like a +failure before the panic continues; the desktop app also guards each UI frame. The browser build can't catch a panic (wasm32 aborts), so there a panic stops the page and its panic hook keeps the files with unsaved changes in local storage for the next page. Junk-parameter sweeps run every command with bad input. ## Revision history diff --git a/docs/roadmap.md b/docs/roadmap.md index c447a6ed..9260eba5 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # FigCraft roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (a development example generates figcraft-cli's shell completions and man pages from its clap definitions (part of #1396); `.fig` ZIP image reads share a total byte budget and count skipped images in the import warnings (#1076); compact layer trees with fields and outline (M0.9, part of #996); stored variable and collection references keep their identity when another object is named like their id (follow-up to #1542); every file a command reads whole by path is a regular file within a cap that fits it: documents 16 GiB, placed images 256 MiB, CLI scripts 1 GiB; a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; the server key is checked by comparing SHA-256 hashes of the presented and the kept key, as the deployment guide says (#1130); opening a file contacts no team library server it names until you check it (#1097); a hostile server can no longer grow a client's memory with made-up people, and a session remembers a bounded number of deleted layer ids (#1121); `doc.list` gives AI agents and plugins the open files' names, not their paths (#1117); with a large selection, frames where nothing changed reuse the selection's outlines, box and slots and the Design panel's instance and position values instead of working them out again (#1571); team libraries and plugins larger than the 16 MiB download burst can be downloaded again, more slowly (#1089); a batch keeps one undo snapshot, not one per step (#1099); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); joining and leaving a file nobody is editing reads and writes nothing on the server: it stays loaded a minute after everyone leaves (#1074); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); Thai, Lao, Khmer and Burmese lines break between words (#1219); an AI agent attached over MCP shows where it works (#1416); a manual overlay's position can be dragged on the canvas (#405); one name for live sessions and comments: Preferences ▸ Your name, in the Join dialog too, kept in the browser, and a rename shows at once in the sessions you are in (#1380); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); the Join dialog shows a join from the click until its file is ready, with Cancel, and a failed join with Try again (#1381); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); SVG import draws each pattern tile once per scale, within the layer budget (#1080); Vietnamese letters typed with combining marks (decomposed) take one font that has every mark, instead of missing-glyph boxes beside the letter (#1478); comment.list reports each message's @mentions (#1415); M8: mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); M2: components can't contain their own instances, directly or through others; `edit.duplicate` returns an id map with `map: true` (#995); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); MCP and control calls name their document (#998); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); the instance-swap menus have a search field (#1338); a refused batch's fixes take time linear in the batch (#1063); the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); the web build's wasm back under its 24 MiB gate (#1472); shadows of images with transparent pixels follow their alpha (#1289); Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); M0.9: optional batch continue-on-error with per-step outcomes (#1386); Alt pressed during a drag duplicates (#1385); variables in paint props (M3, #1000); finding layers without selecting them (M0.9, part of #996); the attached MCP server reads the app's replies with a 512 MiB bound (#1113); a failed batch keeps the undo steps it undid (#1410); Arabic draft catalog; Android Vector Drawable export; a selected unfilled shape drags over a filled layer below it (#1370); text truncation fills the last line, survives resizing, and auto height takes Max lines (#1375, #1376, #1377); M11 widget fields and property menus; M12 Boards: timer and voting; Installed weights of an embedded font family (#1246); `.sketch` import, best effort; Adobe Fonts found on macOS (#298); M12 Boards; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (a panic in a grouped closure outside any command rolls the group back like a failed group: documents, selection, undo and redo, history limit and journal as before (#1085); a development example generates figcraft-cli's shell completions and man pages from its clap definitions (part of #1396); `.fig` ZIP image reads share a total byte budget and count skipped images in the import warnings (#1076); compact layer trees with fields and outline (M0.9, part of #996); stored variable and collection references keep their identity when another object is named like their id (follow-up to #1542); every file a command reads whole by path is a regular file within a cap that fits it: documents 16 GiB, placed images 256 MiB, CLI scripts 1 GiB; a component set's property rows fit the 240 px right panel: variant values wrap two to a line there and every + and − stays in view; the server key is checked by comparing SHA-256 hashes of the presented and the kept key, as the deployment guide says (#1130); opening a file contacts no team library server it names until you check it (#1097); a hostile server can no longer grow a client's memory with made-up people, and a session remembers a bounded number of deleted layer ids (#1121); `doc.list` gives AI agents and plugins the open files' names, not their paths (#1117); with a large selection, frames where nothing changed reuse the selection's outlines, box and slots and the Design panel's instance and position values instead of working them out again (#1571); team libraries and plugins larger than the 16 MiB download burst can be downloaded again, more slowly (#1089); a batch keeps one undo snapshot, not one per step (#1099); files nested 256 levels deep import on the browser's 1 MB stack (#1101); SVG and PDF exports count pattern sources and the renders they embed from the layer's depth, so one cap bounds a whole export (#1082); an explicit variables file path (`variable.import`, the `variables` of `file.open` and `file.import`) is refused past 256 MiB or when it isn't a regular file, as the sibling `variables.json` already was (part of #1114); joining and leaving a file nobody is editing reads and writes nothing on the server: it stays loaded a minute after everyone leaves (#1074); opening a `.figcraft` file unpacks at most 6 GiB in all (2 GiB in the browser), every area together (#1079); hovering the canvas hit-tests once per pointer move, not on every frame, and moving an auto layout frame no longer lays out its contents again on every move (#1571); `.fig` instance expansion no longer walks variable nodes for every instance or copies every outer override row into each nested instance (#1077); `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); grid auto-placement looks for free cells a 64-bit word at a time and skips what blocks it, with the same placements (#1083); with the pen out, Enter or a double-click ends the path and edits the vector with the Move tool instead of drawing on (#1430); a recovery copy made while a version is previewed holds the file, not the version shown; a FigJam `.jam` file opens as a board (#1342); the Actions palette offers only commands that run from it, and no second row reading like a menu item: Place image opens the file dialog (#1574); share links can comment: a third access level beside edit and view, whose holder sees the file live and adds, replies to, reacts to and resolves comments but can't change the design; Preferences ▸ Scroll wheel zoom: a mouse wheel zooms the canvas about the pointer and Cmd/Ctrl+wheel scrolls (#1390); `comment.list {mentions}` lists only the threads that @mention a name (#1415); in the browser an internal error that stops the page keeps its unsaved files, which open again on reload (#1123); boolean operations refuse input past their caps, and a boolean group past them draws without its result (#1104); figcraft-cli parses its arguments with clap, following RFC-0012 (#1396); the selection and hover outlines of rounded rectangles and frames follow their corners (#1506); component properties keep the order they were made in when one is renamed, and Make slot asks for the slot's name first (#1536, #1537); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); the browser build uses the fonts installed on the computer once asked, each read when text uses it (#1051); SVG import reads `color()` and `color-mix()` colours, with a note and a grey for those it can't (#1550); the portable Windows zip keeps its settings, recovery copies and plugins in `config\` beside the executable, starting with an installed copy's preferences; the window starts on other graphics when its first ones fail before the first frame: Direct3D 12 compiles with FXC and no device compiles wgpu's indirect-validation shaders, then Vulkan, OpenGL and lower requirements on the power-saving GPU (#965, #1438); Thai, Lao, Khmer and Burmese lines break between words (#1219); an AI agent attached over MCP shows where it works (#1416); a manual overlay's position can be dragged on the canvas (#405); one name for live sessions and comments: Preferences ▸ Your name, in the Join dialog too, kept in the browser, and a rename shows at once in the sessions you are in (#1380); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); the Join dialog shows a join from the click until its file is ready, with Cancel, and a failed join with Try again (#1381); requests that change a server's files have a budget per address, the server key's too, and join codes are found by their hash (#1075); SVG import draws each pattern tile once per scale, within the layer budget (#1080); Vietnamese letters typed with combining marks (decomposed) take one font that has every mark, instead of missing-glyph boxes beside the letter (#1478); comment.list reports each message's @mentions (#1415); M8: mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); M2: components can't contain their own instances, directly or through others; `edit.duplicate` returns an id map with `map: true` (#995); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); MCP and control calls name their document (#998); sibling placement in live sessions searches instead of counting (#1065); an AI agent attached over MCP comments under its own name, with an Agent label (#1414); the instance-swap menus have a search field (#1338); a refused batch's fixes take time linear in the batch (#1063); the connector tool shows the connector it will make while dragging, ends snapping to attach points (#1387); large images on the canvas draw from a smaller copy, so two 16k images no longer lag (#1483); a file's JPEG thumbnail can't crash the home screen (#1491's progressive JPEG check, now shared); `.fig` import: library text styles linked by asset key, and override rows restating a nested level (#1234); `.fig` import keeps the file's saved line breaks while the text and its box are unedited (#1236); Download a missing free font from the Missing fonts window (`font.download`, #754); the web build's wasm back under its 24 MiB gate (#1472); shadows of images with transparent pixels follow their alpha (#1289); Crop image: Object ▸ Crop image, the right-click menu and the image fill row enter crop mode (`image.crop`, #1464); M0.9: optional batch continue-on-error with per-step outcomes (#1386); Alt pressed during a drag duplicates (#1385); variables in paint props (M3, #1000); finding layers without selecting them (M0.9, part of #996); the attached MCP server reads the app's replies with a 512 MiB bound (#1113); a failed batch keeps the undo steps it undid (#1410); Arabic draft catalog; Android Vector Drawable export; a selected unfilled shape drags over a filled layer below it (#1370); text truncation fills the last line, survives resizing, and auto height takes Max lines (#1375, #1376, #1377); M11 widget fields and property menus; M12 Boards: timer and voting; Installed weights of an embedded font family (#1246); `.sketch` import, best effort; Adobe Fonts found on macOS (#298); M12 Boards; community fixes in #507, #510, #529 and #530: auto-save, a text box by dragging, batch step references, removing someone from a live session, Russian and Turkish catalogs reviewed, Czech and Indonesian drafts, the Japanese draft completed, zoom previews, the web clipboard, Alt-drag snapping and Cmd+D repeat, recovery copies, crash and rollback fixes, colour emoji, `.fig` import fixes; earlier changes are in the revision history below) · **Target:** Figma Design (desktop app 126.10.7) Forward-looking plan. Where FigCraft stands today: [`ROADMAP.md`](../ROADMAP.md) and [`target-app-parity.md`](target-app-parity.md); the full ranked list: [`gaps.md`](gaps.md). Hours @@ -76,6 +76,7 @@ blockers. | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | M8, #1085 (the rest): a panic in a `Session::grouped` closure outside any command (plugin, widget, data fill and CLI groups) skipped the group rollback, leaving documents half edited, the history limit unbounded, live documents held and the group depth raised. `checkpointed` now catches the unwind, runs the same rollback as a failed group and resumes the panic, so the outer guard still reports it. Journal rollover and the browser (wasm aborts) are unchanged. 3 regression tests (`group_panic_tests`). | | 2026-10-30 | minor | M0.9, part of #1396: a development example generates completions for Bash, Elvish, Fish, PowerShell and Zsh and a man page for the CLI and each subcommand, nested ones included, from figcraft-cli's clap definitions, without running commands. Generating these files doesn't install them; package installation and the other RFC-0012 exceptions remain outstanding. Regression tests in `apps/figcraft-cli/tests/it/generated_docs.rs` cover artifacts, command names and options, matching Bash dispatch states, repeat generation and filesystem errors. Numbers unchanged | | 2026-10-30 | minor | M8, #1076: `.fig` ZIP image reads share a total budget, 4 GiB on 64-bit targets and 1 GiB on 32-bit targets, keeping the 256 MiB per-image cap. Members that do not fit are skipped before allocation with an import warning; failed reads consume their bytes, repeated references keep their cached resolution. Embedded message images, decoded pixel memory and native `.figcraft` reads are unchanged. Regression tests in `fig::tests::image_budget` (format). Numbers unchanged. | | 2026-10-30 | minor | M3, follow-up to #1542: stored variable and collection references use exact ids in export, library copies and swaps, imported aliases, alias deletion and cycle checks, Dev Inspect, the library mode picker, and prototype actions and expressions. Interaction edits resolve only incoming names and leave stored references alone. Command arguments still accept names as before. Tests: the `stored_variable_ids_*` tests in the doc, engine, format and ui-egui present and responsive suites. Numbers unchanged |