From 34ba937da8bee0c786814b2f799ffab9b6a26a65 Mon Sep 17 00:00:00 2001 From: BFlat Date: Fri, 30 Oct 2026 09:00:50 +0700 Subject: [PATCH] M9: Comments: mentioned names in comment.people, and mentions in comment.next and comment.prompt (#1591) * M9: comment.list lists only the threads that mention a name (#1415) 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. * M9: comment.people lists mentioned names; comment.next and comment.prompt carry mentions (#1415) comment.people adds every name a message @mentions to those who wrote or reacted (once each in any case). comment.next takes the mentions filter and returns the thread as comment.list lists it; comment.prompt's bounded comments carry each thread's mentions (at most 32, names cut to 80 characters). One mention helper serves them all; builds on #1585. * M9: comment.people lists people who wrote in two cases once The participant list is deduplicated in any case too, not only the names added from mentions, in one pass (CodeRabbit on #1591). Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- ROADMAP.md | 3 +- crates/engine/src/cmd/comment.rs | 109 +++++++++++++++----- crates/engine/src/comment_mentions_tests.rs | 64 ++++++++++++ crates/engine/src/comments_polish_tests.rs | 3 +- docs/collaboration-parity.md | 3 +- docs/mcp.md | 13 ++- docs/roadmap.md | 3 +- 7 files changed, 161 insertions(+), 37 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index ae107e02..51b3e5fa 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~26 points and ~162–292 h away (one Opus 5.5 agent; ~33–67 h wall-clock with 5 agents) -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-29 · **Change:** minor (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); mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); components can't contain their own instances, directly or through other components; `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); offline Penpot 2.x import/export (#1143); optional batch continue-on-error with per-step outcomes (#1386); the Design panel's sections on one grid (#858); FigJam outer shape outlines and sticky body fallback beside author text (#1341, #1397); images placed at their own size, 2x screenshots in points (#1360); the toolbar on one 32 pt grid, without its Resources button (#1210); M4: zero-length round-capped dashes draw dots; dash expansion uses the existing safety budget; extracted from #1143; Arabic draft catalog; scaled instances keep their scale when their main is rebuilt (#922); .fig embedded images are copied once per blob, not once per hash (#1059); page ruler guides survive save and reopen (#1476); colour picker LCH and OKLCH models; non-ASCII layer names transliterate in code export (#1235); branches in live sessions and branch history entries, gap 17; plugin widgets take text input and a property menu (plugin API 4); boards: templates and tables; boards: a shared timer and voting sessions; boards: the dot grid, connector ends dragged to re-attach, curved connectors; Installed weights of an embedded font family (#1246); `.sketch` import, best effort (Sketch 48 and newer); CJK line breaking: JIS X 4051's line-start and line-end sets completed with the halfwidth small kana, the halfwidth and spacing (semi-)voiced sound marks, the iteration marks 〱–〵 and the halfwidth parentheses, with behaviour cases for each; Adobe Fonts found on macOS (#298); egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; 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-29 · **Change:** minor (`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); mask groups draw only over the area they can paint, and nested isolated layers share one memory budget (#1061); components can't contain their own instances, directly or through other components; `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); offline Penpot 2.x import/export (#1143); optional batch continue-on-error with per-step outcomes (#1386); the Design panel's sections on one grid (#858); FigJam outer shape outlines and sticky body fallback beside author text (#1341, #1397); images placed at their own size, 2x screenshots in points (#1360); the toolbar on one 32 pt grid, without its Resources button (#1210); M4: zero-length round-capped dashes draw dots; dash expansion uses the existing safety budget; extracted from #1143; Arabic draft catalog; scaled instances keep their scale when their main is rebuilt (#922); .fig embedded images are copied once per blob, not once per hash (#1059); page ruler guides survive save and reopen (#1476); colour picker LCH and OKLCH models; non-ASCII layer names transliterate in code export (#1235); branches in live sessions and branch history entries, gap 17; plugin widgets take text input and a property menu (plugin API 4); boards: templates and tables; boards: a shared timer and voting sessions; boards: the dot grid, connector ends dragged to re-attach, curved connectors; Installed weights of an embedded font family (#1246); `.sketch` import, best effort (Sketch 48 and newer); CJK line breaking: JIS X 4051's line-start and line-end sets completed with the halfwidth small kana, the halfwidth and spacing (semi-)voiced sound marks, the iteration marks 〱–〵 and the halfwidth parentheses, with behaviour cases for each; Adobe Fonts found on macOS (#298); egui 0.37 hidden-capture deadline contract; Dev Mode measurements kept on the canvas and a read-only layers panel; boards: a board area row and progress entry; 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) FigCraft is a clean-room, open-source, pure-Rust reimplementation of the Figma design workflow, native on macOS, Windows, Linux and FreeBSD and in the browser, fully drivable by agents through @@ -355,6 +355,7 @@ Ranked; detail and milestones in [`docs/roadmap.md`](docs/roadmap.md). | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | M9, #1415 (the rest): `comment.people` lists every name a message @mentions besides those who wrote or reacted (once each in any case, a known name spelt as the file spells it, never an email address), so a name that has only been tagged shows up. `comment.next` takes the `mentions` filter and, like `comment.open`, returns the thread as `comment.list` lists it, with each message's `mentions`; `comment.prompt`'s bounded `comments` carry each thread's mentions (at most 32, names cut to 80 characters). One mention helper serves them all. Numbers unchanged. Tests: `comment_mentions_tests`. | | 2026-10-30 | minor | M8, #1083: grid auto-placement tested every cell of a child's span at every position it tried (children × rows × columns × span area), so a crafted grid of wide children kept every layout pass busy. The grid's taken cells are now a bitmap: a span is checked a 64-bit word at a time, the search jumps past the cell that blocks it, a row's search ORs the span's rows once, and a column-anchored child looks on from where the last one of the same column and spans found room (cells only ever get taken). Placements are the same, checked against the old search on 400 random grids. A crafted 1000 × 1000 grid with 1000 full-width children: 192 ms → 0.3 ms; 3000 column-anchored wide children: 4.2 ms → 0.7 ms; ordinary grids unchanged (20 000 1 × 1 children 5.2 ms either way). Numbers unchanged. Tests: `grid_place_tests`. | | 2026-10-30 | minor | M1, #1430: from the pen into vector edit mode. A double-click with the pen drew on: its second press added a point on top of the last one (a zero-length segment), a double-click meant to edit a layer drew a stray two-point vector on it, and Enter ran what Escape does. Now a press on the pen's last anchor adds nothing; a double-click while drawing ends the path at that point, and one that only started a path drops it and edits the layer under the pointer; Enter ends the path. Both go on editing the vector with the Move tool (points and handles); the next Enter leaves edit mode. In edit mode the pen's double-click and Enter take the Move tool too, a double-click taking back the point or new path its first press began. Escape still keeps the pen in edit mode for the next path (#760). Numbers unchanged. Tests: `pen_vector_edit_tests`. | | 2026-10-30 | minor | M1: desktop recovery copies made while a version is previewed held the version shown instead of the file, so a crash during a preview brought the old version back as the unsaved file. What a recovery copy holds is now one shared helper, `figcraft_engine::branches::recovery_copy`: the document itself (`DocState::document_itself`, the same document the unsaved-changes check compares, not the version or live text preview shown in its place), its version history and its branches. The desktop copies and the browser's (#1580, which already took the document itself) both use it. Tests: `recovery_tests.rs` in the desktop and web apps (a copy made during a version preview recovers the file and its history). | diff --git a/crates/engine/src/cmd/comment.rs b/crates/engine/src/cmd/comment.rs index 6dab7e8e..cf675d0f 100644 --- a/crates/engine/src/cmd/comment.rs +++ b/crates/engine/src/cmd/comment.rs @@ -346,27 +346,65 @@ fn thread_mentions(c: &Comment, people: &[&str], name: &str) -> bool { c.messages().any(|m| mentioned(&m.text, &people).any(|(n, _)| n.eq_ignore_ascii_case(name))) } -/// `comment.list`'s `mentions` filter: a name (a leading `@` is fine), or none. -fn mentions_param(p: &Value) -> crate::Result> { +/// A list's `mentions` filter: a name (a leading `@` is fine), or none. +fn mentions_param(cmd: &str, p: &Value) -> crate::Result> { match get(p, "mentions") { None | Some(Value::Null) => Ok(None), Some(Value::String(s)) if !s.trim().trim_start_matches('@').trim().is_empty() => { Ok(Some(s.trim().trim_start_matches('@').trim().to_string())) } - Some(_) => Err(bad("comment.list", "`mentions` must be a name")), + Some(_) => Err(bad(cmd, "`mentions` must be a name")), } } +/// The threads a list's params pick (`list_params`, `frame`, `mentions`), in list order, as +/// `who` sees them; mentions resolve against `people` (`mentionable`). +fn listed<'s>(cmd: &str, s: &'s Session, p: &Value, who: &str, people: &[&str]) -> crate::Result> { + let (page, filter, sort, text) = list_params(cmd, s, p)?; + let want = mentions_param(cmd, p)?; + let q = Query { page, frame: id(p, "frame"), filter, sort, text: &text }; + Ok(query(s.doc()?, &q, who).into_iter().filter(|c| want.as_deref().is_none_or(|w| thread_mentions(c, people, w))).collect()) +} + +/// Add the names `text` @mentions to `named` as `{name, known, you}`, each once. +fn push_mentions(named: &mut Vec, text: &str, people: &[&str], user: &str) { + for (name, known) in mentioned(text, people) { + let v = json!({"name": name, "known": known, "you": known && name.eq_ignore_ascii_case(user)}); + if !named.contains(&v) { + named.push(v); + } + } +} + +/// The most mentions `comment.prompt` lists for one thread (its list stays bounded). +const PROMPT_MAX_MENTIONS: usize = 32; + +/// `comment.people`: everyone who wrote or reacted in a comment, and every name a message +/// @mentions (as `comment.list` reports it), once each in any case, sorted as +/// `Document::comment_people` sorts. +fn people_and_mentioned(doc: &Document, you: &str) -> Vec { + let people = doc.comment_people(); + let resolve = mentionable(&people, you); + let mut seen = std::collections::HashSet::new(); + let mut out: Vec = Vec::new(); + let names = people + .iter() + .map(String::as_str) + .chain(doc.comments.iter().flat_map(Comment::messages).flat_map(|m| mentioned(&m.text, &resolve).map(|(n, _)| n))); + for name in names { + if seen.insert(name.to_lowercase()) { + out.push(name.to_string()); + } + } + out.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()).then_with(|| a.cmp(b))); + out +} + /// A message as JSON, with the names it @mentions: `{name, known, you}` each, resolved against /// `people` (`mentionable`); `known: false` is a name nobody in the file has. fn message_json(m: &Message, people: &[&str], user: &str) -> Value { let mut named: Vec = Vec::new(); - for (name, known) in mentioned(&m.text, people) { - let v = json!({"name": name, "known": known, "you": known && name.eq_ignore_ascii_case(user)}); - if !named.contains(&v) { - named.push(v); - } - } + push_mentions(&mut named, &m.text, people, user); json!({ "author": m.author, "agent": m.agent, "text": m.text, "created": m.created, "edited": m.edited, "reactions": m.reactions.iter().map(|r| json!({"emoji": r.emoji, "users": r.users})).collect::>(), @@ -395,9 +433,23 @@ pub fn comment_json(doc: &Document, c: &Comment, user: &str, people: &[&str]) -> /// Bounded metadata for `comment.prompt`'s `comments` list. Full messages and replies stay in /// `comment.list`; a prompt over thousands of threads must not repeat all of them here. -fn comment_brief_json(doc: &Document, c: &Comment, user: &str) -> Value { +fn comment_brief_json(doc: &Document, c: &Comment, user: &str, people: &[&str]) -> Value { let (x, y) = c.position(doc); let frame = frame_of(doc, c); + // The names the whole thread @mentions, as `comment.list` reports them per message. + let mut named = Vec::new(); + for m in c.messages() { + push_mentions(&mut named, &m.text, people, user); + if named.len() >= PROMPT_MAX_MENTIONS { + break; + } + } + named.truncate(PROMPT_MAX_MENTIONS); + for v in &mut named { + if let Some(name) = v.get("name").and_then(Value::as_str).map(|n| clip_chars(n, 80)) { + v["name"] = json!(name); + } + } json!({ "id": c.id, "page": c.page, "pageName": doc.node(c.page).map(|n| clip_chars(&n.name, 80)), @@ -405,7 +457,7 @@ fn comment_brief_json(doc: &Document, c: &Comment, user: &str) -> Value { "frame": frame, "frameName": frame.and_then(|f| doc.node(f)).map(|n| clip_chars(&n.name, 80)), "resolved": c.resolved, "unread": c.is_unread(user), "link": link(c.id), "author": clip_chars(&c.first.author, 80), "agent": c.first.agent, "preview": clip_chars(&c.first.text, 160), - "replies": c.replies.len(), + "replies": c.replies.len(), "mentions": named, }) } @@ -784,15 +836,13 @@ pub fn specs() -> Vec { spec!( "comment.next", "Next comment", - "{id? (the thread to step from; omitted: the first or last), dir? (1 next, -1 previous; default 1), page?, filter?, sort?, query?, frame?, resolved? (the list, as comment.list), author?}", + "{id? (the thread to step from; omitted: the first or last), dir? (1 next, -1 previous; default 1), page?, filter?, sort?, query?, frame?, resolved?, mentions? (the list, as comment.list), author?} → the thread, as comment.list lists it", false, |s, p| { let dir = num(p, "dir").unwrap_or(1.0); - let doc = s.doc()?; let who = author(s, p); - let (page, filter, sort, text) = list_params("comment.next", s, p)?; - let ids: Vec = - query(doc, &Query { page, frame: id(p, "frame"), filter, sort, text: &text }, &who).iter().map(|c| c.id).collect(); + let people = s.doc()?.comment_people(); + let ids: Vec = listed("comment.next", s, p, &who, &mentionable(&people, &who))?.iter().map(|c| c.id).collect(); let at = id(p, "id").and_then(|i| ids.iter().position(|x| *x == i.0)); let next = match at { Some(i) if dir < 0.0 => ids.get(i.saturating_sub(1)), @@ -825,9 +875,16 @@ pub fn specs() -> Vec { let id = req_comment("comment.link", s, p)?; Ok(json!({"link": link(id)})) }), - spec!("comment.people", "People in comments", "{}", false, |s, _| { - Ok(json!({"people": s.doc()?.comment_people(), "you": acting_name(s)})) - }), + spec!( + "comment.people", + "People in comments", + "{} → {people (everyone who wrote or reacted in a comment, and every name a message @mentions), you}", + false, + |s, _| { + let you = acting_name(s); + Ok(json!({"people": people_and_mentioned(s.doc()?, &you), "you": you})) + } + ), spec!( "comment.list", "List comments", @@ -835,17 +892,11 @@ pub fn specs() -> Vec { false, |s, p| { let doc = s.doc()?; - let (page, filter, sort, text) = list_params("comment.list", s, p)?; - let want = mentions_param(p)?; let who = author(s, p); - let q = Query { page, frame: id(p, "frame"), filter, sort, text: &text }; let people = doc.comment_people(); let people = mentionable(&people, &who); - let list: Vec = query(doc, &q, &who) - .into_iter() - .filter(|c| want.as_deref().is_none_or(|w| thread_mentions(c, &people, w))) - .map(|c| comment_json(doc, c, &who, &people)) - .collect(); + let list: Vec = + listed("comment.list", s, p, &who, &people)?.into_iter().map(|c| comment_json(doc, c, &who, &people)).collect(); Ok(json!({"comments": list})) } ), @@ -906,7 +957,9 @@ pub fn specs() -> Vec { }; let count = selected.len(); let (text, truncated) = prompt_text(doc, path.as_deref(), &selected); - let comments: Vec = selected.iter().take(PROMPT_MAX_THREADS).map(|c| comment_brief_json(doc, c, &who)).collect(); + let people = doc.comment_people(); + let people = mentionable(&people, &who); + let comments: Vec = selected.iter().take(PROMPT_MAX_THREADS).map(|c| comment_brief_json(doc, c, &who, &people)).collect(); Ok(json!({"count": count, "truncated": truncated, "text": text, "comments": comments})) } ), diff --git a/crates/engine/src/comment_mentions_tests.rs b/crates/engine/src/comment_mentions_tests.rs index 94581029..03e1bbe3 100644 --- a/crates/engine/src/comment_mentions_tests.rs +++ b/crates/engine/src/comment_mentions_tests.rs @@ -77,3 +77,67 @@ fn the_mentions_param_lists_only_the_threads_that_mention_a_name() { assert!(s.execute("comment.list", &json!({"mentions": junk})).is_err(), "{junk}"); } } + +#[test] +fn people_lists_the_names_only_mentioned_too() { + let mut s = Session::new(); + let id = add(&mut s, "@ada lovelace, @Zoe and @zoe: see me@example.com", "Sam"); + s.execute("comment.reply", &json!({"id": id, "text": "On it", "author": "Ada Lovelace"})).unwrap(); + s.execute("comment.react", &json!({"id": id, "emoji": "👍", "author": "Lin"})).unwrap(); + let p = s.execute("comment.people", &json!({})).unwrap(); + // Ada once, spelt as the file spells her; Zoe once in any case; no email address. + assert_eq!(p["people"], json!(["Ada Lovelace", "Lin", "Sam", "Zoe"])); + assert_eq!(p["you"], "You"); +} + +#[test] +fn people_who_write_in_two_cases_are_listed_once() { + let mut s = Session::new(); + let id = add(&mut s, "Hi", "Ada"); + s.execute("comment.reply", &json!({"id": id, "text": "Hi @ADA", "author": "ada"})).unwrap(); + let p = s.execute("comment.people", &json!({})).unwrap(); + assert_eq!(p["people"].as_array().map(Vec::len), Some(1), "{}", p["people"]); +} + +#[test] +fn next_and_prompt_carry_the_mentions_too() { + let mut s = Session::new(); + let first = add(&mut s, "@Claude make this bigger", "Sam"); + let second = add(&mut s, "Spacing looks off", "Sam"); + s.execute("comment.reply", &json!({"id": second, "text": "@Claude and @Bob, tighten it", "author": "Ada"})).unwrap(); + let plain = add(&mut s, "No mentions here", "Sam"); + let claude = json!({"name": "Claude", "known": false, "you": false}); + // comment.next returns the thread as comment.list lists it, mentions and all. + let list = s.execute("comment.list", &json!({})).unwrap(); + let ids: Vec = list["comments"].as_array().unwrap().iter().map(|c| c["id"].as_u64().unwrap()).collect(); + let next = s.execute("comment.next", &json!({"id": ids[0]})).unwrap(); + let listed = list["comments"].as_array().unwrap().iter().find(|c| c["id"] == next["id"]).unwrap(); + assert_eq!(next["mentions"], listed["mentions"]); + assert_eq!(next["replies"], listed["replies"]); + // Stepping through the threads that mention Claude skips the others. + let n = s.execute("comment.next", &json!({"mentions": "claude"})).unwrap(); + let m = s.execute("comment.next", &json!({"mentions": "claude", "id": n["id"]})).unwrap(); + let mut both = vec![n["id"].as_u64().unwrap(), m["id"].as_u64().unwrap()]; + both.sort_unstable(); + assert_eq!(both, vec![first, second]); + assert!(s.execute("comment.next", &json!({"mentions": 5})).is_err()); + // comment.prompt: each thread's mentions, the first message's and the replies' together. + let prompt = s.execute("comment.prompt", &json!({})).unwrap(); + let mentions = |id: u64| prompt["comments"].as_array().unwrap().iter().find(|c| c["id"] == id).unwrap()["mentions"].clone(); + assert_eq!(mentions(first), json!([claude])); + assert_eq!(mentions(second), json!([claude, {"name": "Bob", "known": false, "you": false}])); + assert_eq!(mentions(plain), json!([])); +} + +#[test] +fn a_prompt_lists_a_bounded_number_of_short_mentions() { + let mut s = Session::new(); + let many: String = (0..100).map(|i| format!("@n{i} ")).collect(); + let id = add(&mut s, &format!("{many} @{}", "x".repeat(500)), "Sam"); + let prompt = s.execute("comment.prompt", &json!({"ids": [id]})).unwrap(); + let mentions = prompt["comments"][0]["mentions"].as_array().unwrap().clone(); + assert_eq!(mentions.len(), 32); + let id = add(&mut s, &format!("@{}", "y".repeat(500)), "Sam"); + let long = s.execute("comment.prompt", &json!({"ids": [id]})).unwrap(); + assert!(long["comments"][0]["mentions"][0]["name"].as_str().unwrap().chars().count() <= 81); +} diff --git a/crates/engine/src/comments_polish_tests.rs b/crates/engine/src/comments_polish_tests.rs index f064d33a..c6690ce4 100644 --- a/crates/engine/src/comments_polish_tests.rs +++ b/crates/engine/src/comments_polish_tests.rs @@ -137,7 +137,8 @@ fn mentions_and_people() { let c = add(&mut s, 0.0, 0.0, "Ask @Ada", "Sam"); s.execute("comment.react", &json!({"id": c, "emoji": "👀", "author": "Lin"})).unwrap(); let p = s.execute("comment.people", &json!({})).unwrap(); - assert_eq!(p["people"], json!(["Lin", "Sam"])); + // Ada was only @mentioned, and is listed too (#1415). + assert_eq!(p["people"], json!(["Ada", "Lin", "Sam"])); assert_eq!(p["you"], "You"); // A thread you're mentioned in is yours. assert!(comment(&s, c).involves("Ada")); diff --git a/docs/collaboration-parity.md b/docs/collaboration-parity.md index 46a7dc7f..242826f1 100644 --- a/docs/collaboration-parity.md +++ b/docs/collaboration-parity.md @@ -1,6 +1,6 @@ # Collaboration parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (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; `comment.list {mentions}` lists only the threads that @mention a name (#1415); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); an AI agent attached over MCP shows where it works (#1416); 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); 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); comment.list reports each message's @mentions (#1415); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); 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); a refused batch's fixes take time linear in the batch (#1063); image entries are checked as they apply; branches in live sessions and branch history entries; server message limits follow `max_file_mb`; a damaged log generation field keeps flushed edits; the web build joins from a `#join=` link; removing someone from a live session; live-session revert guard; live sessions per document; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; cursor chat and spotlight in live sessions; branch version history and finer list merges; comments and version history behaviour suite and fixes; hosting the server: `wss://` in the desktop app, server releases, deployment guide; team libraries on the server and swap library; branches: create, update from main, review and merge; wording: the standalone server with share links to edit or view; numbers unchanged) · **Target:** Figma Design (desktop app 126.10.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (`comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); 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; `comment.list {mentions}` lists only the threads that @mention a name (#1415); the browser build publishes files to a server, so Share ▸ Online and Publish to server work there (#917); an AI agent attached over MCP shows where it works (#1416); 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); 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); comment.list reports each message's @mentions (#1415); the batches kept for reconnecting apps are bounded in bytes by default, and `figcraft-server` takes `max_log_mb` (#1069); 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); a refused batch's fixes take time linear in the batch (#1063); image entries are checked as they apply; branches in live sessions and branch history entries; server message limits follow `max_file_mb`; a damaged log generation field keeps flushed edits; the web build joins from a `#join=` link; removing someone from a live session; live-session revert guard; live sessions per document; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; cursor chat and spotlight in live sessions; branch version history and finer list merges; comments and version history behaviour suite and fixes; hosting the server: `wss://` in the desktop app, server releases, deployment guide; team libraries on the server and swap library; branches: create, update from main, review and merge; wording: the standalone server with share links to edit or view; numbers unchanged) · **Target:** Figma Design (desktop app 126.10.7) Figma is multiplayer first; FigCraft is local first. **Ready ~13% (estimated):** comments, version history and branches (review and merge) work inside one file; the real-time collaboration core landed in #103 (sync model, @@ -29,6 +29,7 @@ cursors, selections, text carets, cursor chat and spotlights shown live ([`colla | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | M9, #1415 (the rest): `comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry each thread's mentions as `comment.list` does. Numbers unchanged. | | 2026-10-30 | minor | M9, #1525: share links can comment. A third access level, `comment`, sits between `edit` and `view`: someone who joins with a comment link (or its join code) sees the file live, as with a view link, and adds comment threads, replies, reactions and resolves them; any other edit is refused, by their app and by the server, which 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, and the Share popover offers Can edit, Can comment and Can view; the toolbar shows a "Can comment" badge with Move, Hand and Comment. `collab.share {access: "comment"}`, `collab.manageFile {action: createLink, access: "comment"}` and `figcraft-server links create FILE --access comment` make and pick it. Older apps are unaffected: one that joins with a comment link is welcomed as a viewer, links listed to it read as view links, and a level an app doesn't know reads as view; an older server makes no comment link and sharing goes on without one. Comment authorship is the name in Preferences, as before (no accounts), so a commenter can edit or remove other people's threads. Tests: `comment_access_tests.rs` (collab), `comment_link_tests.rs` (engine, server end to end), the Share popover and toolbar in `live_share_tests.rs` and `live_server_tests.rs`. | | 2026-10-30 | minor | M9, part of #1415: `comment.list {mentions}` lists only the threads where a message @mentions a name, combined with the other filters, so an agent tagged in comments reads just its threads. Numbers unchanged. | | 2026-10-30 | minor | M9, #917: the browser build publishes to a `figcraft-server`. Share ▸ Online (the server in Preferences) and Share ▸ Publish to server… upload the open file over the page's own WebSocket, make the links and join codes others join with, and keep the tab in sync with the new file, as the desktop app does; asking a server about its hosting, stopping a share and sharing again go out the same way. The Share popover offers only online sharing there (a page can't listen, so the local network stays the desktop app's); without a server in Preferences it says so and offers joining. A failed publish says why and leaves the file as it was. The browser keeps only your name across reloads, so the owner token of a file it put on an open server lasts as long as the page (`crates/collab-net/src/exchange.rs`, run by the browser client's driver; the requests and what their answers mean are shared with the desktop transport in `requests.rs`). Tests: `exchange_tests` (scripted server: with and without the key, sharing or not, refusals, an old server without join codes, time-outs, leaving while publishing, the background requests), `live_join_tests`; checked in a browser against a local server. Percentages and hours unchanged | diff --git a/docs/mcp.md b/docs/mcp.md index 343bf5f1..28730001 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1181,7 +1181,8 @@ and read state are its own, it can edit and delete only its own messages, and it Preferences is unchanged. It is set once per connection, so no command needs `author`; an `author` param still wins. In the app, comments act as the name from Preferences (`pref.userName {name}`, default "You"); only a message's author can edit or delete it. `@Name` in a message mentions -someone (`comment.people` lists the names used in the file and, as `you`, who is acting). +someone (`comment.people` lists the names used in the file: everyone who wrote or reacted, and +every name a message @mentions, once each in any case; and, as `you`, who is acting). Read state is kept per name: a new message is unread for everyone but its author; `comment.markRead {id? | link?, read?}` marks one thread (or, without `id`, all of them) read or @@ -1200,8 +1201,9 @@ those `mentions` report it, combined with the other filters: `{"mentions": "Clau (`"Zoe Smith"` finds `@Zoe Smith`). Links are `figcraft://comment/`: `comment.link {id}` returns one, `comment.open {id | link, view?}` goes to its page, zooms to its frame (`fit`, the default; `center` keeps the zoom; `keep` doesn't move) -and opens it in the app, and `comment.next {id?, dir?, …list params}` steps to the next or previous -thread of a list the same way. `comment.zoom {ids}` zooms in on a group of pins. Comment ids made in +and opens it in the app, and `comment.next {id?, dir?, …list params}` (`mentions` too) steps to the +next or previous thread of a list the same way. Both return the thread as `comment.list` lists it, +with each message's `mentions`. `comment.zoom {ids}` zooms in on a group of pins. Comment ids made in a live session are large numbers (their creator's id space): pass them back as given. An agent can read every open thread as one resource (`figcraft://comments`) or call @@ -1209,8 +1211,9 @@ An agent can read every open thread as one resource (`figcraft://comments`) or c query?, includeResolved?}` returns `{count, truncated, text, comments}`, where `text` is a Markdown task brief with the document, each thread's page, frame, anchor, point, messages, replies and link, and the instructions to fix, reply and resolve. `comments` is bounded metadata for at most 200 of -the selected threads (id, page, frame, anchor, status, link, author, preview and reply count), not -the full threads: use `comment.list` for those. The Comments panel's **Copy for AI** action uses +the selected threads (id, page, frame, anchor, status, link, author, preview, reply count and +`mentions`: the names the whole thread @mentions, as `comment.list` reports them per message, at most +32 with names cut to 80 characters), not the full threads: use `comment.list` for those. The Comments panel's **Copy for AI** action uses it. The `fix_comments` prompt runs the review loop: inspect the anchored layer, make the smallest change (one `batch` per thread), reply with what changed, verify with a screenshot, then resolve. Reply before resolving: `comment.reply` reopens a thread, so resolving first and replying diff --git a/docs/roadmap.md b/docs/roadmap.md index 05f042e1..9c119549 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-29 · **Change:** minor (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-29 · **Change:** minor (`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 @@ -75,6 +75,7 @@ blockers. | Date | Change | Summary | |---|---|---| +| 2026-10-30 | minor | M9, #1415 (the rest): `comment.people` lists names that were only @mentioned; `comment.next` (with the `mentions` filter) and `comment.prompt` carry the same mentions as `comment.list`. Tests: `comment_mentions_tests` | | 2026-10-30 | minor | M8, #1083: grid auto-placement on an occupancy bitmap (word-at-a-time span checks that jump past the blocking cell), the same placements as before; a crafted 1000 × 1000 grid 192 ms → 0.3 ms. Tests: `grid_place_tests` | | 2026-10-30 | minor | M1, #1430: with the pen out (drawing, idle or in edit mode), Enter or a double-click ends the path and edits the vector with the Move tool, without a duplicate point or a stray vector; Escape keeps the pen (#760). Tests: `pen_vector_edit_tests` | | 2026-10-30 | minor | M1: desktop recovery copies made while a version is previewed held the version shown instead of the file, so a crash during a preview brought the old version back as the unsaved file. What a recovery copy holds is now one shared helper, `figcraft_engine::branches::recovery_copy`: the document itself (`DocState::document_itself`, the same document the unsaved-changes check compares, not the version or live text preview shown in its place), its version history and its branches. The desktop copies and the browser's (#1580, which already took the document itself) both use it. Tests: `recovery_tests.rs` in the desktop and web apps (a copy made during a version preview recovers the file and its history). |