From 8d74210b4d157ae43352b8d36d21bde9612cf736 Mon Sep 17 00:00:00 2001 From: Vladyslav Sitalo Date: Mon, 6 Jul 2026 18:20:38 -0700 Subject: [PATCH 1/9] Add block range annotations design doc --- docs/block-range-annotations-design.html | 537 +++++++++++++++++++++++ 1 file changed, 537 insertions(+) create mode 100644 docs/block-range-annotations-design.html diff --git a/docs/block-range-annotations-design.html b/docs/block-range-annotations-design.html new file mode 100644 index 000000000..c36cb0a1c --- /dev/null +++ b/docs/block-range-annotations-design.html @@ -0,0 +1,537 @@ + + + + + + Block Range Annotations Design + + + +
+ + +

Block Range Annotations Design

+

A proposed model for Google Docs-style comments and future edit suggestions over selected block text. The design assumes individual blocks are usually small and that the app's durable unit of text ownership is the block, not a single document-wide text stream.

+ +

Goal

+

Let a user select text inside one block, or across a small range of blocks, and create a comment thread attached to that selection. The selected text should be highlighted in read mode and edit mode. The comment thread should be discoverable from the target block, behave like normal user content, and leave room for later accept/reject edit suggestions.

+ +

Non-goals

+ + +

Current Substrate

+

The app already has most of the useful surfaces, but they should be composed carefully.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AreaExisting shapeImplication
ReferencesBlockReference stores target id, alias, and optional sourceField; normalization treats references as a set. See blockData.ts.Use references for comment discovery, not for range anchoring.
Backlink indexblock_references is trigger-maintained from references_json with source_field. See references/localSchema.ts.A ref-typed property can make comment threads appear as typed backlinks to target blocks.
Read renderingMarkdown rendering is extended through markdownExtensionsFacet. See MarkdownContentRenderer.tsx.Read-mode highlights can be generated from annotation anchors without changing source content.
Edit renderingCodeMirror extensions are contributed per block. See codeMirrorExtensions.ts and CodeMirrorContentRenderer.tsx.Edit-mode highlights should be CodeMirror decorations over resolved block-local ranges.
SyncCurrent generated PowerSync config streams workspaces, workspace_members, and blocks. See powersync/sync-config.yaml.Block-backed threads avoid a first-pass sync/E2EE table migration.
+ +

Core Decisions

+
+
+

Threads are blocks

+

Comment and suggestion threads should be normal block subtrees. Their content, replies, authorship, deletion, sync, and E2EE behavior then reuse the existing block model.

+
+
+

Anchors are payloads

+

The selected range is metadata on the thread root. It is not embedded in markdown and not stored inside BlockReference.

+
+
+

Discovery uses refs

+

A ref-list property on the thread root points at all target blocks. The existing reference projection gives typed backlinks via sourceField.

+
+
+

Decorations are derived

+

Highlights, markers, side panels, and suggestion diffs are UI derived from anchors. They are not storage.

+
+
+ +

Proposed Data Model

+

Each comment thread root is a block with a dedicated type and a small property payload.

+ +
types: ["annotation:thread"]
+content: "Initial comment text"
+properties: {
+  "annotation:kind": "comment",
+  "annotation:status": "open",
+  "annotation:targetBlocks": ["block-a", "block-b"],
+  "annotation:anchor": {
+    "version": 1,
+    "segments": [
+      {
+        "blockId": "block-a",
+        "from": 12,
+        "to": 31,
+        "exact": "selected text",
+        "prefix": "short text before",
+        "suffix": "short text after",
+        "contentHash": "sha256:..."
+      }
+    ]
+  }
+}
+children: reply blocks
+ +

annotation:targetBlocks should be a refList property. That makes thread roots visible through the existing reference machinery, while annotation:anchor stores the range selectors.

+ +
+ Architectural decision: the backlink edge and the text anchor are separate concepts. The backlink edge answers "which blocks does this thread relate to?" The anchor answers "which bytes of those block contents should be decorated?" +
+ +

Anchor Shape

+

Because blocks are usually small, anchors should be block-local and hybrid: offsets for the normal fast path, exact quote plus context for repair.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldMeaningWhy it exists
blockIdThe block this segment decorates.Keeps anchoring local to durable block identity.
from, toOffsets in the same coordinate system used by the block editor.Cheap resolution and direct CodeMirror decoration.
exactThe selected source text.Lets the anchor survive small edits and detect stale ranges.
prefix, suffixShort surrounding context.Disambiguates repeated text within a small block.
contentHashHash of the block content at anchor creation or last repair.Fast trust path for unchanged content.
+ +

A multi-block selection stores one segment per selected block. The UI can group those segments as one thread, but resolution stays per block.

+ +

Resolution Algorithm

+

Anchor resolution should be simple and deterministic. Small blocks mean brute-force search is acceptable.

+ +
+
+ 1. Hash match + If the content hash matches, trust from/to. +
+
+ 2. Offset check + If content.slice(from, to) === exact, keep the stored offsets. +
+
+ 3. Exact search + Search for exact inside the same block. +
+
+ 4. Context tie-break + If multiple matches exist, rank by prefix and suffix match. +
+
+ 5. Stale fallback + If no unique match exists, mark the segment stale. +
+
+ +

Resolved ranges feed decorations. Stale anchors still keep the thread attached to their target blocks, but no text highlight is shown for the stale segment until it is repaired.

+ +

Anchor Maintenance

+

For a first implementation, anchors can be repaired lazily at render time and persisted only when the user edits or explicitly repairs a stale anchor. A later pass can add automatic remapping on block content edits.

+ +

If automatic remapping is needed, prefer a bounded same-transaction processor. Same-tx processors run inside the user's write transaction and can amend writes atomically, so an undo step can cover both the content edit and anchor update. See sameTxProcessor.ts.

+ +
+ Latency guard: automatic remapping must stay bounded. If a block has too many annotation segments, or repair is ambiguous, the processor should stop early and mark affected segments stale rather than making content saves feel slow. +
+ +

UI Model

+

The UI should present comments as a layer over normal blocks, not as extra inline markdown syntax.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SurfaceBehavior
CreationWhen a non-collapsed selection exists, expose an add-comment command from the editor toolbar, command palette, or compact floating affordance.
Read modeResolved segments render as subtle inline highlights. A margin marker or block-level count opens the relevant thread list.
Edit modeResolved segments render as CodeMirror decorations. Selection and typing stay native to CodeMirror.
Thread panelClicking a highlight or marker opens a side panel or popover showing the thread root and reply children.
Backlinks viewComment threads should not pollute the default linked references view. Use a specialized comments section filtered by sourceField = "annotation:targetBlocks".
Resolved commentsResolved threads remain blocks, but are hidden or collapsed by default in annotation surfaces.
+ +

Edit Suggestions

+

Edit suggestions can use the same thread and anchor model with a different kind and a patch payload.

+ +
{
+  "kind": "suggestion",
+  "status": "open",
+  "anchor": { "version": 1, "segments": [...] },
+  "patch": {
+    "op": "replace",
+    "blockId": "block-a",
+    "from": 12,
+    "to": 31,
+    "deletedText": "old text",
+    "insertedText": "new text"
+  }
+}
+ +

Accepting a suggestion should re-resolve the anchor, verify that the current text still matches the expected deleted text, then perform a normal block content update and mark the suggestion accepted in the same user-visible operation. Rejection only changes suggestion status.

+ +
+ Scope recommendation: start with single-block replace suggestions. Multi-block structural suggestions should wait until there is a clear operation model for block splitting, merging, and subtree edits. +
+ +

Storage Options Considered

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OptionProsConsDecision
Thread blocks with anchor propertyUses existing block sync, undo, E2EE, deletion, and child replies. Easy to query through refList backlinks.Anchor payload is less queryable than a table; thread metadata updates rewrite the thread block properties.Recommended v1.
Target block stores all annotationsSimple to find annotations for one block.Every comment churns the target block row; properties can grow unbounded; undo/sync couples annotation edits to target content.Reject.
Synced annotations tableBest query shape and isolated annotation churn.Requires hosted schema, RLS, upload/apply path, PowerSync stream, E2EE handling, and migration work.Defer until scale demands it.
Inline hidden markersAnchors move with source text.Pollutes markdown, leaks implementation details into export/copy, and makes normal editing harder.Reject.
+ +

Implementation Slices

+
    +
  1. Define annotation schemas: thread type, target refList property, anchor payload property, status/kind properties.
  2. +
  3. Add anchor utilities: create segments from selection, resolve segment against block content, mark stale, and repair by exact/context search.
  4. +
  5. Add query helpers for "annotation threads targeting block X" via the existing reference index and sourceField.
  6. +
  7. Add read-mode highlights, probably through a markdown extension or content decorator that receives resolved ranges.
  8. +
  9. Add edit-mode highlights through a CodeMirror decoration extension.
  10. +
  11. Add create-comment flow from an editor selection and render a minimal thread panel.
  12. +
  13. Add resolve/reopen actions and stale-anchor UI.
  14. +
  15. Add single-block replace suggestions once comments are stable.
  16. +
+ +

Testing Plan

+ + +

Open Questions

+ + +

Related Prior Art

+ +
+ + From bc926782f694cd68bfac3c4c79b3cdbe503c8b2e Mon Sep 17 00:00:00 2001 From: Vladyslav Sitalo Date: Mon, 6 Jul 2026 21:35:00 -0700 Subject: [PATCH 2/9] Refine block range annotations design --- docs/block-range-annotations-design.html | 141 ++++++++++++++++------- 1 file changed, 102 insertions(+), 39 deletions(-) diff --git a/docs/block-range-annotations-design.html b/docs/block-range-annotations-design.html index c36cb0a1c..51b1e84fb 100644 --- a/docs/block-range-annotations-design.html +++ b/docs/block-range-annotations-design.html @@ -232,7 +232,7 @@

Block Range Annotations Design

A proposed model for Google Docs-style comments and future edit suggestions over selected block text. The design assumes individual blocks are usually small and that the app's durable unit of text ownership is the block, not a single document-wide text stream.

Goal

-

Let a user select text inside one block, or across a small range of blocks, and create a comment thread attached to that selection. The selected text should be highlighted in read mode and edit mode. The comment thread should be discoverable from the target block, behave like normal user content, and leave room for later accept/reject edit suggestions.

+

Let a user select text inside one block and create a comment thread attached to that selection. The selected text should be highlighted in read mode and edit mode where the renderer can map raw offsets safely. The data model should also allow a future block-range selection to store one segment per block. The comment thread should be discoverable from the target block, behave like normal user content, and leave room for later accept/reject edit suggestions.

Non-goals