mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-31 05:46:46 -06:00
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 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
393abf8075
commit
34ba937da8
+2
-1
File diff suppressed because one or more lines are too long
@@ -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<Option<String>> {
|
||||
/// A list's `mentions` filter: a name (a leading `@` is fine), or none.
|
||||
fn mentions_param(cmd: &str, p: &Value) -> crate::Result<Option<String>> {
|
||||
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<Vec<&'s Comment>> {
|
||||
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<Value>, 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<String> {
|
||||
let people = doc.comment_people();
|
||||
let resolve = mentionable(&people, you);
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut out: Vec<String> = 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<Value> = 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::<Vec<_>>(),
|
||||
@@ -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<CommandSpec> {
|
||||
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<u64> =
|
||||
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<u64> = 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<CommandSpec> {
|
||||
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<CommandSpec> {
|
||||
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<Value> = 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<Value> =
|
||||
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<CommandSpec> {
|
||||
};
|
||||
let count = selected.len();
|
||||
let (text, truncated) = prompt_text(doc, path.as_deref(), &selected);
|
||||
let comments: Vec<Value> = 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<Value> = 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}))
|
||||
}
|
||||
),
|
||||
|
||||
@@ -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<u64> = 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);
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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 |
|
||||
|
||||
+8
-5
@@ -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/<id>`: `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
|
||||
|
||||
+2
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user