Skip to content

Suggest mode 5/9: inline suggestion operations - #80431

Open
adamsilverstein wants to merge 69 commits into
suggest/inline-markersfrom
suggest/inline-ops
Open

adamsilverstein wants to merge 69 commits into
suggest/inline-markersfrom
suggest/inline-ops

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Jul 17, 2026 •

Copy link
Copy Markdown
Member

Part of #73411

What's in this PR

Adds the suggestion-specific layer on top of the marker primitive: create,
accept, and reject inline text (add/del) and formatting suggestions
(operations.js); the pure reconcilers that turn a text edit or a format
toggle into marker actions (reconcile-edit.js, reconcile-format.js); word/
line delete-range resolution; strip-markers for clean attribute snapshots;
findSuggestionText/findMarkerText helpers; and the annotate-suggestions hook.
Still pure modules - the keyboards and reconcilers that call them into the
live editor land in the final step.

Diagram

Pure transforms over a rich-text value: write a marker, resolve it on accept or reject, strip it for output.

Inline suggestion operations grouped into write, resolve and strip, with a worked type-over example

Testing

This is one layer of the stack. To exercise the whole feature, #78994 bundles every layer into one branch and builds it in Playground:

Test in WordPress Playground

👉 https://playground.wordpress.net/gutenberg.html?pr=78994

Enable Gutenberg > Experiments > Collaboration > Suggestion Mode, then follow the walkthrough in #73411, which also explains how to review the stack layer by layer.


Suggest mode stack

This rebuilds the manually-stacked Suggest mode work (#73411) as a GitHub Stack of 9 small, independently reviewable PRs, each building on the one below it:

  1. #80427 - editor intent (edit/suggest/view) + experiment gate
  2. #80428 - suggestion storage, REST controller, provider
  3. #80429 - block-level capture (attribute + structural)
  4. #80430 - inline marker primitive
  5. #80431 - inline suggestion operations
  6. #80432 - review UI (Apply/Reject sidebar + summary)
  7. #80433 - inline live wiring
  8. #82047 - architecture documentation
  9. #82048 - end-to-end test suite

Each follow up fix now sits in the layer that owns the code it changes, rather than piling onto the top of the stack. The whole feature can be exercised end-to-end via the combined testing branch #78994 (Playground). Behind the "Suggestion Mode" experiment (Gutenberg > Experiments).


AI Use

Code and description both written with 🤖 Claude Code. I will review and test.

Adds the suggestion-specific layer on top of the marker primitive: create,
accept, and reject inline text (add/del) and formatting suggestions
(operations.js); the pure reconcilers that turn a text edit or a format
toggle into marker actions (reconcile-edit.js, reconcile-format.js); word/
line delete-range resolution; strip-markers for clean attribute snapshots;
findSuggestionText/findMarkerText helpers; and the annotate-suggestions hook.
Still pure modules - the keyboards and reconcilers that call them into the
live editor land in the final step.
@github-actions

github-actions Bot commented Jul 17, 2026 •

Copy link
Copy Markdown

Size Change: 0 B

Total Size: 7.93 MB

compressed-size-action

@adamsilverstein
adamsilverstein marked this pull request as ready for review August 5, 2026 22:24
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: adamsilverstein <adamsilverstein@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

github-actions Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Flaky tests detected in d690668.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/33117688052
📝 Reported tests:

refuses the drop and uploads nothing in /test/e2e/specs/editor/various/single-file-placeholder-drop.spec.js, passed after 1 failed attempt.
Error: expect(received).toHaveLength(expected)

Expected length: 0
Received length: 2
Received array:  [{"_links": {"about": [{"href": "http://localhost:8889/wp-json/wp/v2/types/attachment"}], "author": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/users/1"}], "collection": [{"href": "http://localhost:8889/wp-json/wp/v2/media"}], "curies": [{"href": "https://api.w.org/{rel}", "name": "wp", "templated": true}], "replies": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/comments?post=106"}], "self": [{"href": "http://localhost:8889/wp-json/wp/v2/media/106", "targetHints": {"allow": ["GET", "POST", "PUT", "PATCH", "DELETE"]}}], "wp:attached-to": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/posts/104", "id": 104, "post_type": "post"}]}, "alt_text": "", "author": 1, "caption": {"rendered": ""}, "class_list": ["post-106", "attachment", "type-attachment", "status-inherit", "hentry", "entry"], "comment_status": "open", "date": "2026-08-27T21:35:42", "date_gmt": "2026-08-27T21:35:42", "description": {"rendered": "<p class=\"attachment\"><a href='http://localhost:8889/wp-content/uploads/2026/08/10x10_e2e_test_image_z9T8jK-1.png'><img loading=\"lazy\" decoding=\"async\" width=\"10\" height=\"10\" src=\"http://localhost:8889/wp-content/uploads/2026/08/10x10_e2e_test_image_z9T8jK-1.png\" class=\"attachment-medium size-medium\" alt=\"\" style=\"width:100%;height:100%;max-width:10px;\" /></a></p>
"}, "featured_media": 0, "filename": "10x10_e2e_test_image_z9T8jK-1.png", "filesize": 80, "guid": {"rendered": "http://localhost:8889/wp-content/uploads/2026/08/10x10_e2e_test_image_z9T8jK-1.png"}, "id": 106, "link": "http://localhost:8889/?attachment_id=106", "media_details": {"file": "2026/08/10x10_e2e_test_image_z9T8jK-1.png", "filesize": 80, "height": 10, "image_meta": {"alt": "", "aperture": "0", "camera": "", "caption": "", "copyright": "", "created_timestamp": "0", "credit": "", "focal_length": "0", "iso": "0", "keywords": [], "orientation": "0", "shutter_speed": "0", "title": ""}, "sizes": {}, "width": 10}, "media_type": "image", "meta": [], "mime_type": "image/png", "modified": "2026-08-27T21:35:42", "modified_gmt": "2026-08-27T21:35:42", "ping_status": "closed", "post": 104, "slug": "10x10_e2e_test_image_z9t8jk-2", "source_url": "http://localhost:8889/wp-content/uploads/2026/08/10x10_e2e_test_image_z9T8jK-1.png", "status": "inherit", "template": "", "title": {"rendered": "10x10_e2e_test_image_z9T8jK"}, "type": "attachment"}, {"_links": {"about": [{"href": "http://localhost:8889/wp-json/wp/v2/types/attachment"}], "author": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/users/1"}], "collection": [{"href": "http://localhost:8889/wp-json/wp/v2/media"}], "curies": [{"href": "https://api.w.org/{rel}", "name": "wp", "templated": true}], "replies": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/comments?post=105"}], "self": [{"href": "http://localhost:8889/wp-json/wp/v2/media/105", "targetHints": {"allow": ["GET", "POST", "PUT", "PATCH", "DELETE"]}}], "wp:attached-to": [{"embeddable": true, "href": "http://localhost:8889/wp-json/wp/v2/posts/104", "id": 104, "post_type": "post"}]}, "alt_text": "", "author": 1, "caption": {"rendered": ""}, "class_list": ["post-105", "attachment", "type-attachment", "status-inherit", "hentry", "entry"], "comment_status": "open", "date": "2026-08-27T21:35:42", "date_gmt": "2026-08-27T21:35:42", "description": {"rendered": "<p class=\"attachment\"><a href='http://localhost:8889/wp-content/uploads/2026/08/10x10_e2e_test_image_z9T8jK.png'><img loading=\"lazy\" decoding=\"async\" width=\"10\" height=\"10\" src=\"http://localhost:8889/wp-content/uploads/2026/08/10x10_e2e_test_image_z9T8jK.png\" class=\"attachment-medium size-medium\" alt=\"\" style=\"width:100%;height:100%;max-width:10px;\" /></a></p>
"}, "featured_media": 0, "filename": "10x10_e2e_test_image_z9T8jK.png", "filesize": 80, "guid": {"rendered": "http://localhost:8889/wp-content/uploads/2026/08/10x10_e2e_test_image_z9T8jK.png"}, "id": 105, "link": "http://localhost:8889/?attachment_id=105", "media_details": {"file": "2026/08/10x10_e2e_test_image_z9T8jK.png", "filesize": 80, "height": 10, "image_meta": {"alt": "", "aperture": "0", "camera": "", "caption": "", "copyright": "", "created_timestamp": "0", "credit": "", "focal_length": "0", "iso": "0", "keywords": [], "orientation": "0", "shutter_speed": "0", "title": ""}, "sizes": {}, "width": 10}, "media_type": "image", "meta": [], "mime_type": "image/png", "modified": "2026-08-27T21:35:42", "modified_gmt": "2026-08-27T21:35:42", "ping_status": "closed", "post": 104, "slug": "10x10_e2e_test_image_z9t8jk", "source_url": "http://localhost:8889/wp-content/uploads/2026/08/10x10_e2e_test_image_z9T8jK.png", "status": "inherit", "template": "", "title": {"rendered": "10x10_e2e_test_image_z9T8jK"}, "type": "attachment"}]
    at /home/runner/work/gutenberg/gutenberg/test/e2e/specs/editor/various/single-file-placeholder-drop.spec.js:71:45

Accepting an addition, rejecting a deletion or accepting a format
suggestion removed the suggestion format across the whole range from
the id's first fragment to its last, so another suggestion sitting in
the gap was resolved along with it. Unwrap only the runs that carry the
resolved id, sharing the run walk removeMarkedRange already used.

Claude-Session: https://claude.ai/code/session_01EmSXGAGtuLnk3UEe5kXPSU
@adamsilverstein

Copy link
Copy Markdown
Member Author

Claude reproduced this one first, then fixed it:

Fixed in a8f4487. unwrapMarker() stripped the suggestion format across the whole first-to-last-fragment range, so a different suggestion in the gap was resolved too. It now unwraps only the runs carrying the resolved id, using the same run walk removeMarkedRange() already relied on (now a shared runsCarryingId() helper). New tests cover add#1 A / add#2 B / add#1 C for accepting the addition and the matching deletion reject. Both failed before the change.

adamsilverstein and others added 27 commits September 28, 2026 23:25
The prefix/suffix trim worked on UTF-16 code units, so changing an emoji
next to one with the same high surrogate left lone halves in the
removed and inserted text, which then reached saved content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHz7zkCC2S8crriSWsYPDk
The appliers leave anything but RichTextData untouched, so a plain
string attribute got a plan, a new note, and no marker. Returning no
plan keeps those edits on the overlay path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHz7zkCC2S8crriSWsYPDk
Planning and applying a marker serialized each RichTextData value to
HTML and parsed it back, about four cycles per keystroke. Read the
record RichTextData already holds, scan formats instead of HTML for the
marker probes, and bail out of the format planner as soon as the text
differs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHz7zkCC2S8crriSWsYPDk
Reading `RichTextData` directly left `toRichTextRecord` returning a
union with the class's `never[]` array types, so every rich-text
helper call on its result failed to type-check, and `hasSuggestionMarkers`
could not iterate the formats at all. Declare the record as a
`RichTextValue` and type the scanned formats the same way.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MnVknLuyvKfR9DsM4H3SrX
A format marker nests inside the formatting it proposes, and an inline
image inserted as a suggested addition loses its image. Both tests fail
until the reconcilers are fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011duzHshRK8Jud89VQ6BZZB
…ions

applyFormat nests a new format inside the formats a range shares, so a
bolded run saved as <strong><mark>…</mark></strong>. Stamping the marker
outside the run's formatting lets the front end swap the whole span for
the original run.

An inline image is an object replacement character with no formats, so
the edit reconciler rebuilt the inserted run as plain text and dropped
the image. Serialize the run whenever it carries objects, leave out only
an inherited suggestion marker, and pass the HTML when growing the
author's own addition as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011duzHshRK8Jud89VQ6BZZB

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Notes Phase 3 of the Gutenberg roadmap around block commenting [Package] Editor /packages/editor [Type] Feature New feature to highlight in changelogs.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants