24c2c33dc7
Sidecar comments with quote-first anchors, CriticMarkup suggestions through the OT path, per-member feed; first three tasks sized for MiniMax. Written by MiniMax M3.1 Flash (OpenCode Zen free route) with web research notes; three owner questions open. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
7.8 KiB
7.8 KiB
Research: comments, suggestions, mentions, activity feed
Browsed 2026-10-20. Primary sources only; anything unverified is marked.
1. What the repo already gives us (no new dependency needed)
- OT over plain text:
crates/collab(L1) exportsTextOp,Component,diff,transform,transform_index,Selection,Client,Room,Presence(crates/collab/src/lib.rs:41). - Vault I/O:
Vault::{read, write, write_private, list, delete_to_trash}(crates/vault/src/lib.rs:251). - Sync: every file in the vault is syncable; only
.notescraft,.trash,.git,.obsidianare excluded (crates/sync/src/path.rs:30); a conflict becomes a copy (crates/sync/src/conflict.rs:12);merge3merges three texts (crates/sync/src/merge.rs:30). - Command surface:
CommandSpec+ParamSpec(crates/engine/src/spec.rs,crates/engine/src/params.rs:88), write gatecrate::commands::writable(crates/engine/src/commands.rs:156), assistant gate listsDELETE_COMMANDS/WRITE_COMMANDS/READ_COMMANDS(crates/engine/src/ai/assistant_gate.rs:21,60,147). - Members of a vault:
GET /v1/vaults/{vault}/members(apps/notescraft-sync-server/src/api/routes.rs:53), clientShareClient::membersreturningMember { user_id, name, color, role }(crates/engine/src/share/client.rs:82,244), commandshare.members(crates/engine/src/share/commands.rs:60). - Audit log:
GET /v1/audit(apps/notescraft-sync-server/src/api/routes.rs:48), tableapps/notescraft-sync-server/src/store/audit.rs. - Server-Sent Events: parser
SseParser(crates/engine/src/live/sse.rs), server streamapps/notescraft-sync-server/src/live/events.rs; the live route is per note path (.../api/routes.rs:82). - Markdown:
parse,Document::replace_range,block_ids(crates/markdown/src/lib.rs:242,299,347). - UI: right card tabs
RightTab(crates/ui-egui/src/panels/right_view.rs:103), phone pagesCompactPage(crates/ui-egui/src/shell/compact/mod.rs:29), phone sheetSyncSheet(crates/ui-egui/src/panels/sync_sheet/mod.rs:88).
2. CriticMarkup (the suggestion layer)
- Five marks: addition
{++ ++}, deletion{-- --}, substitution{~~ ~> ~~}, comment{>> <<}, highlight{== ==}. Compatible with Markdown/MultiMarkdown/HTML. https://github.com/CriticMarkup/CriticMarkup-toolkit - Pandoc renders and resolves it with
--critic-mode=accept|reject|diff|markup; the marks are generated by a word-level diff (diff-match-patch, the library behind Google Docs). https://github.com/ickc/pancritic https://github.com/google/diff-match-patch - Known limits from the spec itself: avoid newlines inside a mark, wrap Markdown tags completely, marks of the same type do not nest, and comments cannot overlap. A fork that adds ids and overlapping comment ranges has to extend the syntax. https://github.com/jbearak/manuscript-markdown/blob/main/docs/criticmarkup.md
- Consequence for us: CriticMarkup carries no author, no timestamp, no thread, no resolved state. It is a change-marking format, not a discussion format.
3. Obsidian plugins: three real storage shapes
- Document Comments (kylemcd): comments live inside the Markdown as HTML comments, with an
anchor pair
<!--c:id-->text<!--/c:id-->and a thread block<!--co:id by:kyle at:… status:open quote:"text"-->. Threaded replies, resolve/reopen, reactions, Markdown in bodies; HTML comments are hidden by Markdown renderers, so Obsidian shows a clean note. https://github.com/kylemcd/obsidian-document-comments - Marginalia (Tsuuuuuuun): comments live in sidecar JSON files, one per note, never touching
the
.md. Anchoring is a 3-stage match (line hint -> context-scored search -> fuzzy match), and comments whose text vanished become orphaned and are shown in their own filter. https://github.com/Tsuuuuuuun/obsidian-marginalia - Obelisk: comments in the frontmatter, and the rule that matters: "The quote is the anchor. A line number is never an anchor" - the exact quote is searched in the note, the line number is only a tiebreak between two identical quotes. https://obelisk.typeworks.dev/
4. Notion
- A comment hangs from a parent (
page_idorblock_id), never from an arbitrary text range: "The public API can create comments attached to whole blocks withparent.block_id. It does not support creating a new discussion anchored to a selected range of text inside a block." https://developers.notion.com/reference/create-a-comment - A thread is a
discussion_id; replies carry the same id. State isresolvedon the discussion. Bodies are rich text with mentions that point at stable ids (mention.type = "user",user.id), so a rename does not break a mention. https://developers.notion.com/reference/comment-object https://developers.notion.com/reference/block
5. Google Docs / Drive
- Comments are not in the document: they are Drive objects with a
quotedFileContent(the text the comment is about) and an opaqueanchor(a JSON region). Status isopen/resolved, and resolving is done by posting a reply.mentionedEmailAddresseslists the mentions. https://developers.google.com/workspace/drive/api/reference/rest/v3/comments https://developers.google.com/workspace/drive/api/guides/manage-comments - Suggestions are "deferred edits waiting for approval" (Docs API), i.e. the same object model as a comment plus a pending edit. https://developers.google.com/workspace/docs/api/how-tos/suggestions
6. Word / OOXML
- Track changes are wrappers in the file:
w:ins(author, date, id) andw:del. Accepting removes the wrapper and keeps the content; rejecting deletes the inserted content and restores the deleted one. Comments anchor withw:commentRangeStart/w:commentRangeEnd+w:comment. https://ooxml.org/wordml/w-ins https://ooxml.org/wordml/w-del https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.commentrangestart
7. Anchor drift
- GitHub anchors review comments to diff hunks and line numbers, so a comment that refers to moved text is shown as outdated and stays reachable but detached from the new text. https://github.blog/news-insights/pull-request-diff-comments
- A product that anchors to the text itself describes the practical rule: anchors re-attach through edits, fuzzy matching survives rewording, and a comment whose text is truly gone becomes an orphan with a re-anchor flow instead of vanishing. https://metavert-cms.fly.dev/compare/github-pull-requests-vs-markupmarkdown
8. Mentions and activity feeds
- Slack deprecated
<@username>and asks for<@W123>: "You should never use name as any primary key. It's always been mutable... Reference user IDs instead." https://docs.slack.dev/changelog/2017-09-the-one-about-usernames - Unread state is per surface: Slack marks a conversation up to a point ("markConversation"), GitHub/Notion show an updates feed with a badge. The event carries the actor, the target and a timestamp; the receiver keeps the read marker, not the event. https://slack.com/help/articles/226410907-View-all-your-unread-messages
9. What we take from it
- The quote is the anchor; line and offset are hints that may go stale (Obelisk, Google Docs).
- A comment that cannot be re-anchored must become orphaned and stay visible with its body (Marginalia, GitHub "outdated").
- CriticMarkup is right for suggestions (a pending edit of the text) and wrong for threads (no ids, no resolve state, no overlapping) - so the two layers need different storage.
- Mentions store the user id, never the display name (Slack, Notion).
- HTML comments inside the
.mdare the Obsidian-compatible inline trick, but they put the discussion inside the note text, which is exactly what the OT room and the publish projection would have to strip.