diff --git a/ROADMAP.md b/ROADMAP.md index da33537ce..61df4900c 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-30 · **Change:** minor (`comment.list {mentions}` lists only the threads that @mention a name (#1415); 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-30 · **Change:** minor (`comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); `comment.list {mentions}` lists only the threads that @mention a name (#1415); 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 | M9, part of #1415: `comment.list` takes `mentions` (a name, any case, a leading `@` allowed) and lists only the threads where a message, the first or a reply, @mentions it, combined with the other filters (`{"mentions": "Claude", "filter": "unread"}`). It matches the names each message's `mentions` reports, by the same parser the thread view highlights with (`figcraft_doc::mentions`), so `@Ada Lovelace` is Ada Lovelace, not Ada, and an email address mentions nobody; the name counts as one of the file's people, so an unknown name can be several words. Numbers unchanged. Tests: `comment_mentions_tests`. | | 2026-10-30 | minor | M8, #1104: boolean operations had no input cap, so a crafted file could keep the sweep busy for unbounded time and memory (any number of segments, crossing as often as they like, and a fold re-sweeping its growing result for every operand). Each sweep now takes at most 250 000 segments and 2 million possible crossings (pairs of segments whose bounds overlap, counted exactly in O(n log n)), and a fold at most 4 million segments swept over all its steps; past them the operation is refused with `TooComplex` ("the shapes are too complex to combine") before it runs. The example files' boolean groups sweep at most 55 segments and 155 possible crossings per step. A boolean group past the caps draws without its result, with a warning, once, like layers past the render's memory budget; the refusal is remembered, so redrawing it costs nothing; Flatten refuses it instead of dropping it. Numbers unchanged. Tests: `budget_tests` (pathops), `boolean_budget_tests` (render), `boolean_cap_tests` (engine). | | 2026-10-30 | minor | M0.9, #1396: `figcraft-cli` parses its arguments with clap, following RFC-0012 (craft-contrib), so help comes from the same definitions the parser uses and bad arguments fail before any work starts. Changed syntax: an option given twice is a usage error (it used to take the last value; the automation roots still repeat), a value starting with `-` needs `=` (`--filter=--json`), `--o` for `-o` and the `version` subcommand are gone (`--version` stays), `--format` is checked before the file is opened, help and usage errors are clap's text (`Usage: figcraft-cli …`, `error: …` with a hint instead of the whole usage). Every subcommand, flag, value form and exit code is otherwise unchanged. Tests: `args::tests` (each documented form, every usage error, every subcommand's help) and the CLI integration tests. Numbers unchanged | diff --git a/crates/engine/src/cmd/comment.rs b/crates/engine/src/cmd/comment.rs index 6dab7e8ef..8d5009fbe 100644 --- a/crates/engine/src/cmd/comment.rs +++ b/crates/engine/src/cmd/comment.rs @@ -346,27 +346,62 @@ 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 out = people.clone(); + for m in doc.comments.iter().flat_map(Comment::messages) { + for (name, _) in mentioned(&m.text, &resolve) { + if !out.iter().any(|p| p.eq_ignore_ascii_case(name)) { + 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 +430,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 +454,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 +833,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 +872,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 +889,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 +954,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 945810292..b409435f7 100644 --- a/crates/engine/src/comment_mentions_tests.rs +++ b/crates/engine/src/comment_mentions_tests.rs @@ -77,3 +77,58 @@ 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 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 f064d33ab..c6690ce4a 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 3694eabf0..1272c733e 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 (`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); `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, 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 | | 2026-10-30 | minor | Where an AI agent works (M8, #1416): an agent attached over MCP (`figcraft-cli mcp --attach`) changed the file with no sign of where or whether it was still at work. Now, while it runs commands, the people row at the top of the right panel shows its avatar, ringed in its own colour and named as its comments are signed ("Claude (agent)"), and the canvas outlines the layers its last edit changed (the innermost ones), or that it last selected, in that colour with its name. Both go 8 seconds after its last command, and only the file it works in shows them. Display only: no undo step, never saved, never sent to a live session's other people, and the user's own commands never make one (`agent_presence_tests` in the engine, `live::agent` tests in the UI). Ready unchanged | diff --git a/docs/mcp.md b/docs/mcp.md index c11eda31f..f90c72349 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 f7167b9d3..8fb1c60b9 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 (`comment.list {mentions}` lists only the threads that @mention a name (#1415); 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 (`comment.people` lists names that were only @mentioned, and `comment.next` and `comment.prompt` carry the mentions too (#1415); `comment.list {mentions}` lists only the threads that @mention a name (#1415); 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 | M9, part of #1415: `comment.list {mentions}` lists only the threads where a message @mentions a name (any case, by the same parser as the highlighting), combined with the other filters. Tests: `comment_mentions_tests` | | 2026-10-30 | minor | M8, #1104: boolean operations refuse input past 250 000 segments, 2 million possible crossings or 4 million segments swept over a fold (`TooComplex`, before the sweep runs); a boolean group past them draws without its result, with a warning, and Flatten refuses it. Tests: `budget_tests`, `boolean_budget_tests`, `boolean_cap_tests` | | 2026-10-30 | minor | M0.9, #1396: `figcraft-cli` parses its arguments with clap, following RFC-0012 (craft-contrib), so help comes from the same definitions the parser uses and bad arguments fail before any work starts. Changed syntax: an option given twice is a usage error (it used to take the last value; the automation roots still repeat), a value starting with `-` needs `=` (`--filter=--json`), `--o` for `-o` and the `version` subcommand are gone (`--version` stays), `--format` is checked before the file is opened, help and usage errors are clap's text (`Usage: figcraft-cli …`, `error: …` with a hint instead of the whole usage). Every subcommand, flag, value form and exit code is otherwise unchanged. Tests: `args::tests` (each documented form, every usage error, every subcommand's help) and the CLI integration tests. Numbers unchanged |