Repository navigation
Suggest mode: core subsystem (intent modes, overlay capture, structural + attribute suggestions) - #79713
Suggest mode: core subsystem (intent modes, overlay capture, structural + attribute suggestions)#79713adamsilverstein wants to merge 30 commits into
Conversation
…al + attribute suggestions) Fresh-stack migration of the Suggest mode foundation onto current trunk, re-sliced from the suggest-mode feature branches into a single reviewable base. Includes the Edit/Suggest/View intent switcher, the in-memory overlay capture + store interceptor, attribute and structural (block remove/insert/ move) suggestions with the v2 declarative payload schema, the suggestion diff/summary, auto-save, the collaboration-sidebar Apply/Reject controls, and the REST/PHP backend (note-suggestion controller + render strip). The inline (in-text marker) layer is split into a separate stacked branch. This is a behaviour-preserving migration; planned refactors (overlay-inline retirement, identity-token interceptor, structural op-descriptor table) follow as separate PRs. Replaces the prior 16-deep stack (see #73411).
Adds a 'Suggestion Mode' experiment (id: gutenberg-suggestion-mode) under a new Collaboration group on the Experiments screen, so the whole feature can be enabled experimentally before final approval. - Registers the experiment in lib/experimental/experiments/load.php. - Bridges window.__experimentalSuggestionMode via gutenberg_enable_experiments(). - Gates the PHP requires (block-suggestions.php render strip + the note- suggestion REST controller) in lib/load.php behind the experiment. - Gates the JS surfaces: the IntentSwitcher menu entry, the suggestion provider mounts (store interceptor + auto-save), and the intent-switch keyboard shortcut handlers, so none activate unless the experiment is on. block-comments.php (Notes) stays ungated; it is shipped, not experimental.
|
Size Change: +14.5 kB (+0.19%) Total Size: 7.69 MB 📦 View Changed
|
- Always require the suggestion REST controller + front-end marker strip instead of gating them on the experiment. Both are inert without suggestion data (only the experiment-gated editor UI creates it), and an always-on strip ensures un-accepted markers never reach the front end. This also keeps the PHP REST controller unit tests working (the class is always defined). The user-facing feature stays gated in JS. - Enable the Suggestion Mode experiment in the intent-switcher and suggestion- mode e2e specs (they drive the now-gated UI). - Drop collab-sidebar/test/hooks.js: it tested the feature's inline-markers refactor of the Notes hooks, which was dropped in favour of trunk's self-contained Notes implementation.
18e39ea to
bb166ee
Compare
… intent menu Rename the more-menu Mode picker entries from the bare verbs Edit/Suggest/ View to their progressive forms Editing/Suggesting/Viewing so each reads as the state the editor is currently in. Update the e2e specs that match those accessible names.
The add/delete/style "golden path" tests assert per-word inline marks (<ins class="has-suggestion-addition"> / <del class="has-suggestion-deletion">) that are produced by the inline-suggestions layer, not the core overlay. On the core branch alone they cannot pass, so move them to the inline spec and leave the core spec covering only core suggestion behavior: snackbar announcements, auto-save, the store interceptor, and the empty-inserted-block guard.
setEditorIntent and getEditorIntent were added as new PUBLIC core/editor APIs backing an experiment - a forever compat commitment against the repo's convention that experimental surface ships behind the store's private API mechanism. Move them to private-actions.js / private-selectors.js (registered via registerPrivateActions / registerPrivateSelectors) and update every consumer to unlock(): the intent switcher, the keyboard shortcut handlers, the suggest-mode subsystem's intent reads, and the tests. use-block-editor-settings.js already read through unlock(). The reducer and state shape are unchanged, so persisted behavior is identical. Remove the two entries from the generated data docs by hand and note the private status in the editor CHANGELOG.
The store interceptor's programmatic writes - the revert that restores a block to its baseline, the pending-insert/move/remove marker writes, and the re-insert of a removed subtree - were plain persistent dispatches. Each one cut an undo level, so Ctrl+Z could undo a revert (re-applying the suggested change to real content) or strip a pending marker while the overlay still held the op, desyncing canvas and suggestion state. Dispatch __unstableMarkNextChangeAsNotPersistent() on the block-editor store immediately before each interceptor-originated dispatch - the established core pattern for programmatic writes. User edits themselves remain persistent; only the interceptor's bookkeeping is exempt. Add tests asserting the revert, the re-insert + marker write, and the pending-insert marker write leave isLastBlockChangePersistent() false.
The editor intent is implemented as a session-scoped reducer, but several places still described the abandoned preferences-store design: the store constants claimed the intent is persisted via @wordpress/preferences and survives reloads, the architecture doc said the intent is stored under core.editorIntent in the preferences store, and edit-post / edit-site registered a dead editorIntent preference default that nothing reads. Remove the dead preference defaults and reword the docs to describe the session-scoped reducer storage, so readers don't hunt for preference plumbing that doesn't exist. The selector/action docblocks were already corrected when the API moved to the private store surface.
- Cap the word-LCS input in the sidebar suggestion summary with the same MAX_DIFF_LENGTH fallback DiffForOperation uses, so a payload near the 64KB limit can't freeze the sidebar with an O(m*n) diff. - Preserve the ORIGINAL from-position when a block already carrying a pending-move marker is moved again; overwriting it with the intermediate position made Reject restore a spot that was itself only a pending suggestion. Covered by a new interceptor test. - Use sprintf() for the 'New block: %s' fallback label instead of a manual replace on the translated string. - Target the Accept button by class instead of aria-label in the suggestion header styles; the label is translated so the attribute selector silently stopped matching in non-English locales. - Compare against the EDITOR_INTENT_VIEW constant instead of the 'view' literal in use-block-editor-settings. - Attach the suggestion auto-save waitForResponse listener BEFORE the edit that starts the debounce in the e2e spec; attaching after races the response on slow CI and hangs the wait for its full timeout. - Correct a stale comment pointing at wordpress-6.9/block-comments.php; the payload size constant mirrors wordpress-7.1/block-suggestions.php. - Derive the Accept button's disabled state and its explanatory reason from one predicate (missing blockClientId counts as a missing target) so the two can't disagree.
# Conflicts: # packages/editor/CHANGELOG.md # packages/editor/src/components/provider/use-block-editor-settings.js
Match the intent switcher's Editing / Suggesting / Viewing labels and say plainly that other users apply or reject the proposed changes, dropping the trailing document-suggestion comparison.
|
Flaky tests detected in 39438d1. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/28844296542
|
…ertion suggestion Typing into a newly-appended empty paragraph produced TWO notes: an inline/overlay content suggestion plus an 'Insert block' one. The content path fired first, and its programmatic metadata.noteId write made the deferred empty block look modified, so the interceptor then registered a second, structural suggestion for the same action. A block that IS the suggestion has no 'before' state to preserve, so its edits now write through to the real attributes and stay part of the single block-insert suggestion: - overlay-context publishes the interceptor's empty-placeholder deferral (markDeferredInsertion / isDeferredInsertion) so the first edit into a deferred block can be recognized outside the interceptor. - The interceptor adopts (rather than overlay-diverts + reverts) attribute changes on blocks that are, or are nested inside, a pending insertion, and forgets a deferred block that is deleted before gaining content instead of proposing its removal. - The overlay HOC writes setAttributes through for deferred blocks and extends the pending-insert pass-through to descendants of an inserted block (children of a suggested-in Group). The inline suggestion keyboards gain matching guards in the stacked inline-markers branch.
…rtion wiring The deferred-insertion context callbacks are identity-stable, so the interceptor closes over them directly instead of the render-written ref pattern (frozen at its current count in eslint suppressions), and the HOC test captures the overlay handle from an effect rather than during render.
…component The SuggestionDiff inline diff preview was exported from the suggestion-mode barrel but never rendered anywhere in the subsystem - only its wordDiff engine is consumed (by the sidebar summary). Extract wordDiff and its caps into word-diff.js and drop the dead component, its tests, and its eslint suppression, so reviewers do not spend time on code the marker-based inline layer replaces. Also align the architecture doc and e2e header with what this layer actually renders (pending-treatment classes + sidebar summary; in-content rendering belongs to the inline-suggestions layer stacked on this PR).
Ports the friendlier Suggestion Mode experiment description (already live on try/suggest-mode-combined) onto the stack so the combined branch carries no stack-external changes.
The accept/reject half of the Suggest mode loop had no e2e coverage, and neither did the block-remove and block-move structural captures. Add a review-flows spec exercising the common flows end to end: deleting and moving blocks in Suggesting intent (pending treatment, move ghost, note summary), then accepting or rejecting each suggestion kind this layer ships (attribute-set, block-remove, block-insert-after, block-move) from the notes sidebar in Editing intent, asserting the canvas, the serialized post content, and the note's resolved state.
…s created When a new suggestion note is saved while a non-notes sidebar (e.g. post or block settings) is open, switch it to the All notes sidebar so the note is immediately visible. A closed sidebar stays closed, and an already-open notes sidebar is left alone.
…ng saves Structural suggestion state (metadata.suggestion markers, pending-insert blocks, a pending move's proposed order) previously lived only in the unsaved block tree: SuggestionSaveLock held the editor save/autosave locks while any of it existed, so the post stayed dirty with saving disabled - trapping the suggester behind a beforeunload warning - and a reload lost the pending state, orphaning its note. Persist the pending state into post_content instead, the structural counterpart of inline markers living in content: - Remove SuggestionSaveLock; saving and autosaving stay available. - Hide un-accepted insertions at render time via a type-aware render_block strip (gutenberg_strip_pending_structural_suggestions), mirroring the inline add-marker strip. pending-remove and pending-move blocks render (their content is real until accepted). - Document the new model plus its limitations (pending-move front-end order, cross-parent move anchors after reload).
Pin the persistence model: a structural suggestion leaves Save draft available and the editor clean after saving; a pending move/remove survives a reload with its note linked and can still be rejected or accepted; an un-accepted insertion never renders on the front end.
Order-diffing alone cannot attribute an adjacent swap: moving B above A produces the same before/after order as moving A below B, and the LCS tie-break could pick either reading. Moving a block up tagged the block it passed instead of the moved one, put the origin ghost at the wrong slot, and each further hop tagged a different displaced sibling - piling up a note per hop instead of updating one. Prefer the reading in which the selected blocks are the movers (a block moved via toolbar, keyboard, or drag stays selected), falling back to the LCS heuristic when the selection does not explain the reorder. Also stop re-capturing a rejected move: reject now batches the marker-clear with the restoring moveBlockToPosition, and the interceptor recognizes that shape (marker in the previous tick, gone on the live block) as the suggestion landing - previously, rejecting a move while in Suggesting intent immediately spawned a phantom move suggestion.
Ctrl+Z right after making a suggestion previously mangled it: the store interceptor re-captured the undo-induced change as a brand-new suggestion, attribute suggestions (held in the overlay, invisible to undo history) ignored undo entirely, and the withdrawn suggestion's note survived as an orphan with nothing left to accept or reject. Make undo suggestion-aware while Suggest intent is active: - A new SuggestionUndoGuard wraps the core-data undo/redo actions. It cancels the most recently captured attribute suggestion by reverting its overlay entry to baseline, withdraws a pending move or insertion the way Reject restores the block (as history-ignored writes, since the async note linkage otherwise resurrects the marker when the block survives the history transaction), and otherwise arms an adoption token so the store interceptor adopts the undo result as the new capture baseline instead of re-capturing it. Suggested removals revert cleanly through the real undo stack because the history transaction replaces the re-inserted block wholesale. - A new SuggestionNoteGC trashes a pending note once an anchor it has observed (structural marker + noteId linkage, or overlay entry) has disappeared, closing the orphaned-note gap documented in suggestions.md. Apply/reject races are excluded via a decisions-in- flight registry in the provider and a pending-status recheck. - The provider's noteId linkage write is marked history: 'ignore' — it is system bookkeeping, and as a tracked write it landed past any history coalescing window and ate the first undo as its own stack item. See the capture-sequence stamps in overlay-context.js for how the guard orders overlay-held suggestions against inline marker captures.
…stions Pin the withdrawal contract for cmd/ctrl+Z after each capture kind the core layer owns: a block removal sheds its pending treatment without spawning new suggestions, an insertion is taken back as one unit without being re-captured as a removal, a move restores the original order and clears its ghost, and a pending attribute change is cancelled outright. In every case the suggestion's note leaves the sidebar — a note whose proposed change no longer exists has nothing left to accept or reject.
Cover the remaining attribute kind from the #73411 testing instructions: text alignment is a block support writing style.typography.textAlign straight to the store, so the store interceptor captures it as an attribute suggestion; accepting lands the alignment in the post content. The pending treatment is the only visible cue while pending — the support's useBlockProps hook reads the (reverted) store attributes, so the proposed alignment renders only once accepted.
# Conflicts: # packages/editor/src/store/constants.ts
|
Superseded by a 7-PR GitHub Stack that breaks this work into smaller, independently reviewable steps: #80427 → #80428 → #80429 → #80430 → #80431 → #80432 → #80433. The end state of the new stack is byte-identical to this branch across every feature file, so no behavior changes - just a finer-grained partition for review. See #73411 for the full map. This PR can be closed once the new stack's CI is green. |
What
Fresh-stack replacement for the Suggest mode core, re-sliced from the prior
16-deep stack onto current
trunkas a single reviewable base. Rationale andmap: #73411.
This is the foundation the inline marker layer (#79714) stacks on. It contains
everything except the inline
<mark>marker layer:an overlay; structural edits apply-and-tag on the live block). The overlay is
a capture/store mechanism only — it never renders inline content diffs; all
in-content rendering of text changes lives in the stacked marker layer
(Suggest mode: inline text and formatting suggestions via markers #79714).
declarative
operations[]payload schema and the move-ghost placeholder.word-diffengine), auto-save, and thecollaboration-sidebar Apply / Reject controls.
experiment (Settings → Experiments → Collaboration), off by default.
Fixes #73411, #77867, #73410
Migrate-first
Behaviour-preserving migration of code that previously lived across #77403–#78412.
Remaining planned refactors (e.g. a structural op-descriptor table) land as
separate follow-ups on this branch (see #73411).
Testing
Enable Settings → Experiments → Collaboration → Suggestion Mode, then use the
Edit/Suggest/View switcher in the post editor. Inline text suggestions come with
#79714. Combined Playground preview: #78994.
Replaces #77403, #77404, #77405, #77406, #78351, #78352, #78353, #78308, #77967,
#77968, #77970, #77971, #77973, #77978, #77979, #78412.