Back to Plate

Slate v2 Focus Ownership Cleanup

docs/plans/2026-05-26-slate-v2-focus-ownership-cleanup.md

53.0.875.5 KB
Original Source

Slate v2 Focus Ownership Cleanup

Objective: Close a user-review-ready Slate Plan for Slate v2 read-only/editable focus ownership in comment mode. First prove the live edit-mode blur bug, then choose the best long-term architecture/DX target for fixing it without hiding another runtime hack in an example or React component.

Goal plan: docs/plans/2026-05-26-slate-v2-focus-ownership-cleanup.md

Template: docs/plans/templates/slate-plan.md

Primary template: docs/plans/templates/slate-plan.md

Applied packs:

  • slate-plan

Completion threshold:

  • Score >= 0.92 with no dimension below 0.85.
  • Every pass row is complete or intentionally skipped with evidence.
  • Related issue and reference ledgers are synchronized for any claimed issue, behavior, public API, or non-claim.
  • Browser proof exists for the comment-mode focus/blur behavior in the live .tmp/slate-v2 workspace.
  • Execution mode, if accepted, starts with a failing Playwright regression for edit-mode click -> header click retaining focus.
  • node .agents/rules/autogoal/scripts/check-complete.mjs docs/plans/2026-05-26-slate-v2-focus-ownership-cleanup.md passes before this planning goal can close.

Verification surface:

  • Planning artifact: this file plus relevant source/research/issue ledger reads in plate-2.
  • Live bug proof: http://localhost:3100/examples/comment-mode through Playwright browsers using .tmp/slate-v2/node_modules.
  • Live source grounding: .tmp/slate-v2/packages/slate-react/** and .tmp/slate-v2/playwright/integration/examples/comment-mode.test.ts.
  • Execution proof, after user acceptance: focused comment-mode Playwright test, focused slate-react typecheck/test, lint fix, then the repo-defined Slate v2 check gate required by the accepted plan.

Constraints:

  • Planning mode only in this activation. No .tmp/slate-v2 implementation or test patches.
  • Prefer the long-term runtime ownership fix over a local route/example patch.
  • Keep raw Slate unopinionated; Plate owns product-level toolbar APIs.
  • Public DX must stay boring: normal editor usage, no special comment-mode ceremony unless the behavior truly needs an explicit API.

Boundaries:

  • Editable planning scope: docs/plans/**, docs/research/**, docs/slate-issues/**, docs/slate-v2/ledgers/**, docs/slate-v2/references/**.
  • Source reads are allowed in .tmp/slate-v2; implementation writes are not allowed until a ready plan is accepted.
  • This plan targets slate-react focus/selection runtime behavior, not Plate comment UI, not Yjs adapter implementation, and not a rewrite of comment anchors.

Blocked condition:

  • Block only if live browser proof, live source reads, and issue-ledger reads become unavailable for three consecutive plan activations. Current lane is not blocked.

Slate Plan lane state:

  • slate_plan_lane_status: complete
  • current_pass: closure-score-and-final-gates
  • current_pass_status: complete
  • next_pass: none
  • next_action: user can accept this plan to start execution mode
  • final_handoff_status: complete

Current verdict:

  • verdict: current fix is dirty
  • confidence: high for bug existence, issue-accounting policy, and behavior boundary; high for runtime lifecycle placement, fanout shape, and maintainer objection handling; high for high-risk proof boundaries and ecosystem maintainer pressure; high for revised architecture direction and final planning closure
  • keep / cut / revise call: cut the read-only-only document listener from EditableDOMRoot; replace it with a central focus-boundary runtime owner
  • reason: the live edit-mode editor keeps DOM focus after outside header click, while the existing patch only handles read-only roots and manually edits DOM focus, DOM selection, and model selection from a component effect

Completion rule:

  • Do not call update_goal(status: complete) while any required checklist item remains unchecked.
  • Do not call update_goal(status: complete) until every Slate Plan completion gate is satisfied and check-complete passes.
  • This file plus the active goal are the durable state.

Start Gates:

GateAppliesEvidence
Skill analysis before editscomplete.agents/skills/slate-plan/SKILL.md read; planning mode forbids Slate v2 source/test patches
Active goal checked or createdcompleteactive goal names this focus-ownership cleanup lane and keeps closure pending
Source of truth read before editscompletelive route, .tmp/slate-v2 source, current plan template, research notes, and issue ledgers read
docs/solutions checked for existing-code workcompletefocus/selection solution notes from 2026-05-20, 2026-05-21, 2026-05-03, 2026-04-24 read
Live .tmp/slate-v2 grounding needed for current-state claimscompletebrowser proof and source line reads recorded below

Work Checklist:

  • Objective includes lane outcome, pass schedule, one-pass-per-activation policy, completion threshold, verification surface, constraints, boundaries, and blocked condition.
  • One-pass-per-activation policy respected: this activation completed only closure score and final gates.
  • Live source grounding recorded for current implementation claims.
  • Issue ledger / ClawSweeper sync applied or skipped with concrete evidence.
  • Research and ecosystem synthesis complete for every external system used as evidence.
  • Intent/boundary record and decision brief complete.
  • Scorecard recorded with evidence; numeric threshold met and final gates closed.
  • Applicable implementation-skill review matrix applied or skipped with concrete reason.
  • Slate maintainer objection ledger complete for the runtime ownership change.
  • Verification workspace gate recorded for the live bug claim.
  • TDD used for behavior/proof changes in execution mode, or marked N/A: N/A for planning-only; execution entry still requires a failing Playwright row before the runtime fix.
  • Browser proof captured for the first browser-surface claim.

Completion Gates:

GateAppliesRequired actionEvidence
Named verification thresholdyesClose remaining plan passes and run check-completescore is 0.92, every dimension is at least 0.91, all pass rows are complete, and the checker is the final command for this closure pass
Slate v2 source/runtime/browser claimyesRecord live .tmp/slate-v2 source and browser proofcurrent-state and research rows record live browser/source proof; no implementation proof is claimed in planning mode
Issue ledger or PR reference changedcompleteSync ledger/reference rows after revision passgitcrawl-v2-sync-ledger.md, issue-coverage-matrix.md, fork-issue-dossier.md, and pr-description.md all preserve zero new fixed/improved claims
Autoreview for uncommitted implementation changesN/AN/A for planning-only; required in execution mode if code changesno .tmp/slate-v2 implementation patch exists in this planning lane
Final user-review handoffyesEmit final handoff only after closure passfinal handoff outline and accepted-plan execution entry are recorded below
Goal plan completeyesRun check-complete[autogoal] complete: docs/plans/2026-05-26-slate-v2-focus-ownership-cleanup.md

Phase / pass table:

PhaseStatusEvidenceNext
Current-state read and initial scorecompletelive bug proof, source ownership read, dirty verdict, initial scorerelated issue discovery
Related issue discoverycompleteledger/cache-first classification: direct rows #3893 and #5004; guardrail fixed rows #4376/#5171; related multi-view row #5537; Android/readOnly row #5034 stays mobile-onlyissue-ledger pass
Issue-ledger passcompleteno-new-fix sync added to issue coverage matrix, gitcrawl v2 sync ledger, fork issue dossier, and PR referenceintent/boundary pass
Intent/boundary and decision briefcompletebehavior law, ownership boundaries, option rejection, and execution proof contract hardenedresearch refresh
Research, ecosystem strategy, live-source refreshcompleteProseMirror, Lexical, React 19.2, Tiptap, Yjs, selection-bridge solution note, and current Slate React runtime owners refreshedpressure passes
Performance/DX/migration/regression/simplicity pressure passescompletelistener fanout, repeated-unit budget, native behavior proof, public API minimalism, migration backbone, RED test order, and simplicity cuts hardenedobjection ledger
Slate maintainer objection ledgercompletestrongest objections converted into acceptance conditions: machinery, native focus semantics, selection preservation, multi-root registry, raw Slate scope, command controls, document ownership, performance, read-only selection, and collab/history non-opshigh-risk pass
High-risk deliberate modecompletepre-mortem expanded into kill switches and exact proof rows for WebKit selection, toolbar command selection, internal controls, multi-root/content-root focus, Shadow DOM/document ownership, IME blur, history/collab non-ops, and test oracle false positivesecosystem maintainer pass
Ecosystem maintainer passcompleteProseMirror, Lexical, Tiptap, React, Plate, Yjs, and Slate v2 maintainer lenses confirm the same target: one internal DOM/focus runtime owner, no raw-Slate product helper, no history/collab side effects, and conservative content-root claimsrevision pass
Revision passcompletestale pass wording, TDD planning status, target-owner labels, scorecard, and final execution queue normalized without changing the architecture decisionissue sync accounting
Issue sync accountingcompletefour accounting artifacts confirm zero new fixed/improved claims, #3893/#5004 remain related proof gates, #4376/#5171 stay exact fixed guardrails, #5034 remains mobile/raw-device only, and #5826/#5538/#5568 statuses are unchangedclosure score and final gates
Closure score and final gatescompletescore threshold met, all pass rows closed, issue/reference sync closed, browser/source planning proof recorded, autoreview marked N/A for planning-only, and final handoff entry recordedfinal handoff

Scorecard:

DimensionWeightScoreEvidence
React 19.2 runtime performance0.200.92Revision pass locks native runtime ownership: no broad React state/effects, no per-block handlers, and React only projects the final focus state
Slate-close unopinionated DX0.200.92Public Editable stays unchanged; the raw command-preservation contract remains normal DOM preventDefault
Plate and slate-yjs migration backbone0.150.91Plate product helpers and slate-yjs adapters are non-goals; required substrate proof is local-only blur with no ops/history/collab export
Regression-proof testing strategy0.200.92Revision pass keeps the RED-first execution queue and blocks closure on Add Comment, WebKit/Firefox, counters, follow-up typing, and local-only side effects
Research evidence completeness0.150.94Current source, issue ledgers, ProseMirror, Lexical, Tiptap, React 19.2, Yjs, Plate, and Slate v2 evidence are aligned
shadcn-style composability and minimalism0.100.92Generic public helpers, data attrs, and policy objects remain cut; the target must delete the dirty component listener
Weighted total1.000.92Complete for planning; score threshold, issue-sync, and final gates are closed

Source-backed architecture north star:

  • target shape: one Slate React focus-boundary runtime owner classifies outside interactions for all editable roots, read-only and editable, then delegates to the same reconciler policy for DOM focus, DOM selection, and model selection.
  • source evidence: EditableDOMRoot owns a read-only-only document listener at .tmp/slate-v2/packages/slate-react/src/components/editable.tsx:349; runtime focus/mouse ownership already flows through .tmp/slate-v2/packages/slate-react/src/editable/runtime-focus-mouse-events.ts:27; focus state mutation already lives in .tmp/slate-v2/packages/slate-react/src/editable/selection-reconciler.ts:227; root runtime/global lifecycle already owns document-level selection/drag listeners in .tmp/slate-v2/packages/slate-react/src/editable/runtime-root-lifecycle.ts:7; useEditableRootRuntime wires event runtime plus lifecycle at .tmp/slate-v2/packages/slate-react/src/editable/runtime-root-engine.ts:265 and .tmp/slate-v2/packages/slate-react/src/editable/runtime-root-engine.ts:331; provider focus subscriptions already project ReactEditor.isFocused from document focusin / focusout in .tmp/slate-v2/packages/slate-react/src/hooks/use-slate-runtime.tsx:613.
  • rejected drift: do not patch comment-mode route blur manually, do not add a Plate-style toolbar API to raw Slate, and do not duplicate outside-click logic per example.
  • migration posture: one runtime editor with many views/content roots remains the right backbone for shared focus, selection, history, undo/redo, normalization, and collaboration.

Public API target:

SurfaceProposed shapeUser-facing DXCompatibility / migrationEvidenceVerdict
outside click blurno new public API for the base behaviorclicking page chrome/header blurs any active Slate Editable like a normal editorbehavior fix only; no app migrationlive comment-mode bug proves missing defaultkeep internal
external command controlspreserve via standard onPointerDown={event => event.preventDefault()} unless issue pass proves a first-class helper is neededtoolbars can keep selection for commands without custom root APIsexisting Slate convention; comment button already uses itcomment-mode.tsx Add Comment needs selection retentionkeep minimal
optional helperdefer useSlateSelectionGuard / data attr until evidence proves repeated raw-Slate needavoid turning raw Slate into Plate UI policycan add later without breaking behaviorno broad evidence yetreject for now

Internal runtime target:

LayerCurrent ownerTarget mechanismAvoidsEvidenceVerdict
outside interactionEditableDOMRoot read-only layout effectfocus-boundary controller registered from useEditableRootRuntime / root global lifecycleread-only-only behavior, per-component DOM surgeryeditable.tsx:349-409, runtime-root-engine.ts:265-335, runtime-root-lifecycle.ts:7-35revise
focus/blur reconciliationselection-reconciler.tsexpose/internalize a blur-clear action used by native blur and outside interactiondivergent DOM/model selection clearingselection-reconciler.ts:227-304keep owner
mouse/focus event pipelineruntime-focus-mouse-events.tsclassify internal editor root, nested editable/internal control, external command control, and inert outside targetsplit focus decisions across React effects and handlersruntime-focus-mouse-events.ts:27-254, input-controller.ts:112-146extend
global root lifecycleruntime-root-lifecycle.ts plus provider focus subscriptionsdocument listener registration belongs in runtime/global lifecycle, not EditableDOMRoot JSXduplicated document listeners per component surfaceruntime-root-lifecycle.ts:7-35, use-slate-runtime.tsx:613-642target owner
side-effect policycommit tags and metadatafocus/DOM-selection/scroll effects are explicit local policy, not accidental focus hackscollaboration/history focus pollutionselection-side-effect-policy.ts:15-30, Yjs researchkeep owner

Hook / component / render DX target:

SurfaceCall-site shapeComposition rulePerformance ruleEvidenceVerdict
Editableunchanged for usersbehavior follows DOM editor expectations by defaultno app-level listener per examplebug is default editor behavior, not app behaviorkeep API
comment-mode Add CommentonPointerDown(event.preventDefault()) remains acceptableexplicit external command preservation, not blur behaviorno re-render subscription neededselection button test already depends on preservationkeep
runtime focus boundaryinternal hook/controllerone owner per document/runtime root groupnative listener owns classification; React projects stateresearch favors centralized DOM bridge ownership; current source already has runtime lifecycle and focus subscriptionstarget

Plate migration-backbone target:

PressureSlate substrate targetPlate adaptation routeNon-goalEvidenceVerdict
comment UI/toolbarspreserve selection for external commands, blur on inert outside clickPlate can wrap toolbar controls with preventDefault/helperraw Slate toolbar design systemcomment-mode button behaviorkeep raw minimal
content roots/synced blocksshared focus/selection/history across root viewsPlate renders richer block UI over raw Slate runtime viewsone editor per blockmulti-root memory/source notes favor one runtime editorkeep one runtime

slate-yjs migration-backbone target:

PressureSlate substrate targetCollaboration routeNon-goalEvidenceVerdict
remote/local focuslocal-only focus boundary decisions, deterministic model opsadapter ignores local blur/focus side effects while syncing document opsYjs adapter in this planprior collab-readiness notes require side-effect policy hookskeep local-only

Intent / boundary record:

  • intent: make Slate React focus/selection behavior feel like a normal editor in comment mode and future same-runtime content-root embeddings.
  • outcome: a user-review-ready plan that starts execution with a failing edit-mode blur test, then fixes ownership centrally without changing public Editable DX.
  • in-scope:
    • inert outside pointer/focus transitions from editable and read-only roots;
    • document.activeElement, ReactEditor.isFocused, native selection, and Slate model-selection policy;
    • external command control selection preservation;
    • internal native controls and nested editables;
    • cross-root/multi-view guardrails for one runtime editor.
  • out-of-scope:
    • comment anchor storage, comment body state, or markdown review metadata;
    • Plate toolbar components, product UI helpers, or shadcn command wrappers;
    • Yjs adapter implementation or remote cursor protocol;
    • making ordinary void descendants traversable;
    • Android/raw-device readOnly closure.
  • behavior law:
    1. Editable root -> inert page chrome: DOM focus must leave the editor, native caret/selection must stop presenting as editor-owned, ReactEditor.isFocused must become false, and editable model selection must remain inactive but restorable unless a later explicit policy says otherwise.
    2. Read-only root -> inert page chrome: DOM focus must leave the root and read-only presentation/native selection may clear, but this cannot weaken editable blur/refocus preservation guaranteed by #4376/#5171.
    3. Editor root -> external command control: event.defaultPrevented remains the default raw-Slate preservation contract; commands may keep the editor selection without a new public Slate toolbar API.
    4. Editor root -> internal native control/nested editable: target classification must use existing internal-control policy and must not steal native focus or import the wrong selection.
    5. Same-runtime content roots: focus changes are local browser side effects, not document operations; history, selection restore, and collaboration substrate stay in one runtime editor.
  • decision boundaries:
    • raw Slate owns default browser-editor focus correctness;
    • slate-react owns focus timing, React event lifecycle, and runtime document listeners;
    • slate-dom owns DOM point/path translation and low-level selection bridge mechanics when needed;
    • Plate owns opinionated controls, styling, and product selection helpers;
    • examples teach the raw contract, not workaround ownership.
  • unresolved user-decision points: none.

Decision brief:

  • principles:
    • browser truth beats React focus flags;
    • one owner for DOM focus/selection transitions;
    • default editor behavior must not require example code;
    • editable blur preserves model selection unless an explicit transaction policy says otherwise;
    • public API stays small until repeated raw-Slate evidence demands otherwise.
  • top drivers:
    • live comment-mode activeElement bug;
    • #3893 / #5004 focus-state proof gates;
    • #4376 / #5171 model-selection guardrails;
    • multi-root/content-root one-runtime direction;
    • command controls must keep selection without turning raw Slate into Plate.
  • viable options:
    1. Keep read-only-only patch and add another editable component-level patch.
    2. Move outside-click handling into a centralized Slate React focus-boundary runtime controller, using existing selection reconciler/internal-target policy.
    3. Expose a new public toolbar/focus API now.
    4. Push it into comment-mode example code.
  • chosen option: option 2.
  • why it wins: it fixes read-only and editable through one owner, respects the existing runtime event/reconciler architecture, keeps Editable API boring, and gives future content roots the same focus boundary instead of another app-owned workaround.
  • rejected alternatives:
    • option 1 keeps the dirty split and duplicates document listeners;
    • option 3 leaks product toolbar policy into raw Slate before evidence proves a public primitive is needed;
    • option 4 treats a package behavior bug as an example bug.
  • consequences: execution must touch runtime focus ownership and tests, not only the comment-mode example. It must preserve #4376/#5171 and Add Comment selection behavior while fixing editable outside blur.
  • follow-ups: run final issue/reference sync accounting, then closure score and final gates.

Issue accounting:

Issue / clusterClaim categoryExact claimWhyProof routeV2 sync ledgerPR line
#3893direct related, no fixed claim yetclicking ordinary external UI must update Slate focus stateexact title is HTML button focus state; current live bug is header click, so button proof is still needed before closurecomment-mode header proof plus ordinary button proofsynced in coverage matrix, gitcrawl ledger, fork dossier, PR referencerelated text only
#5004direct related, no fixed claim yetfocus lifecycle must not stay true or fire when it should notissue is spurious focus semantics; current bug is stale focus after outside clickfocus/blur event counter proof plus activeElement proofsynced in coverage matrix, gitcrawl ledger, fork dossier, PR referencerelated text only
#4376 / cluster 20fixed guardrail, must not regressWebKit blur/refocus must preserve inactive model selection and follow-up typingcentral outside-click code can accidentally clear model selectionrerun existing WebKit document-state proof plus comment-mode follow-up typingsynced as unchanged fixed guardrailno new claim line
#5171 / cluster 20fixed guardrail, must not regressFirefox unfocused editor updates must not import external selectionoutside interaction code can accidentally conflate external DOM selection with editor model selectionrerun existing Firefox document-state proof after centralizationsynced as unchanged fixed guardrailno new claim line
#5537 / cluster 7related multi-view pressure, no fixed claim yetmulti-editor/programmatic focus needs view-local focus/input proofcontent roots and comment-mode both expose focus ownership pressurelater multi-root/content-root focus proofsynced as related multi-view pressurerelated text only
#5034adjacent mobile/readOnly, no claimAndroid readOnly selection-null stays mobile/IME proof, not this web outside-click closurethis plan touches readOnly selection presentation but has no raw-device Android proofleave to mobile/device lane; do not claimsynced as mobile/readOnly unchangednone
#5826/#5538/#5568adjacent focus/scroll/initialization guardrailsfocus-boundary work must not restore stale selection or scroll unexpectedlysame runtime area, different exact reprosexisting huge-document/focus contract proof plus targeted rows if touchedsynced as preserved statusesnone unless behavior changes

Issue-ledger sync status:

  • ClawSweeper related-issue discovery: complete, ledger/cache-first; no broad live GitHub search used
  • generated live gitcrawl rows read: complete for #3893, #5004, #5034, #5171, #5537, #4376 and clusters 7/20
  • manual v2 sync ledger update: complete; docs/slate-issues/gitcrawl-v2-sync-ledger.md records this plan as no new fixed/improved claims
  • fork issue dossier update: complete; docs/slate-v2/ledgers/fork-issue-dossier.md records the surface review
  • issue coverage matrix update: complete; docs/slate-v2/ledgers/issue-coverage-matrix.md records related/non-claim policy
  • PR description sync: complete; docs/slate-v2/references/pr-description.md records zero new fixed/improved claims
  • final accounting pass: complete; the revised plan adds no claim-changing behavior, API, source, or proof beyond the already-synced focus-boundary planning target.

Final issue/reference sync accounting:

ArtifactEvidence readAccounting result
docs/slate-issues/gitcrawl-v2-sync-ledger.mdcomment-mode focus-boundary planning sync states zero fixed/improved claims and lists #3893, #5004, #4376/#5171, #5537, #5034, and #5826/#5538/#5568 policyno edit needed
docs/slate-v2/ledgers/issue-coverage-matrix.mdfocus cleanup planning section says no fixed/improved claims, #3893/#5004 are related, #4376/#5171 stay exact guardrails, #5034 remains mobile-only, and focus/scroll statuses are preservedno edit needed
docs/slate-v2/ledgers/fork-issue-dossier.mdfork dossier section mirrors the same decisions and explicitly says PR counts stay unchangedno edit needed
docs/slate-v2/references/pr-description.mdPR description includes the comment-mode planning sync in the no-new-claims summary with 0 new exact fixed/improved claimsno edit needed

Final accounting verdict:

  • new fixed issue claims: 0
  • new improved issue claims: 0
  • direct related rows kept as related: #3893, #5004
  • fixed guardrails kept exact and unchanged: #4376, #5171
  • adjacent/mobile/focus-scroll statuses unchanged: #5034, #5826, #5538, #5568
  • PR fixed/improved counts unchanged.

Ecosystem strategy synthesis:

SystemSourceMechanismAvoidsStealRejectSlate targetVerdict
ProseMirrordocs/research/sources/editor-architecture/prosemirror-transaction-view-dom-runtime.mdone view-owned DOM observer/selection import/export authoritysplit DOM bridge logiccentralized DOM bridge ownerschema-specific view model or plugin complexityone focus-boundary runtime owner next to selection import/exportuse
Lexicaldocs/research/sources/editor-architecture/lexical-read-update-extension-runtime.mdupdate tags encode history, DOM selection, focus, scroll, and collaboration policyaccidental side effectsexplicit side-effect metadataclass nodes, $ helpers, command-heavy app policylocal focus/selection policy path using commit tags/metadatause selectively
React 19.2docs/research/sources/editor-architecture/react-19-2-external-store-and-background-ui.mdReact projects external-store state; native hot path stays outside broad renderseffect spaghettiexternal-store/runtime boundarycomponent-owned focus hacksruntime first, component thinuse
Tiptapdocs/research/entities/tiptap.mdproduct-layer focus/menu helpers over ProseMirror primitivesraw engine UI sprawlproduct DX pressure onlycopying Tiptap focus helpers into raw Slate corekeep raw Slate substrate; Plate can productize helpersreject as engine owner
Yjs bindingsdocs/research/sources/editor-architecture/yjs-collaboration-bindings.mdcollaboration imports carry metadata so remote/local side effects do not steal focusremote sync polluting local focus/historylocal-only side-effect tags and awareness outside document commitslegacy wrapper mutationfocus-boundary decisions stay local browser effects, not document opsuse
Slate solution notesdocs/solutions/performance-issues/2026-05-08-dom-selection-bridges-must-stay-cheap-on-selectionchange.mdhot DOM selection path stays primitive/privaterich per-event allocations and public classifier leaksprivate finite classification, runtime-root listenerspublic bridge APIcheap internal focus/selection classifieruse

Ecosystem maintainer pressure pass:

Maintainer lensWould acceptWould rejectPlan consequenceVerdict
ProseMirror maintainerone view/runtime-owned DOM bridge with transaction/local-state separationapp commands reading DOM selection directly or Slate copying plugin/view complexityput outside focus boundary beside selection import/export and prove focus blur is not a document operationpasses with constraint
Lexical maintainerupdate metadata/tags for focus, DOM selection, scroll, composition, history, and collaboration policyclass nodes, $ helper culture, dispatch-command UI API, or a full DOM reconciler copyuse existing commit metadata vocabulary; do not expose a new public command/focus APIpasses with constraint
Tiptap maintainerproduct wrappers for focus/menu/comment controls at Plate/app levelraw Slate owning toolbar UI policy because one example needs itkeep preventDefault as raw contract; let Plate add ergonomic wrappers later if repeated evidence appearspasses
React maintainera native runtime listener as the external-system boundary and narrow external-store focus projectioncomponent layout-effect spaghetti, per-root React state, or rerendering editor content on document pointerdownkeep listener/controller internal to root runtime/global lifecycle; React only subscribes to final focus statepasses
Plate maintainerdefault raw Slate correctness plus optional product-layer wrappers laterforcing every Plate plugin/example to carry blur workaround codefix default blur in slate-react; leave Plate API work out of this planpasses
Yjs maintainerfocus/blur as local browser state outside document commits, undo, and remote exportlocal inert blur producing operations, history entries, awareness churn, focus steal, or scroll on remote importadd execution proof for zero writes/history/collab side effects on inert blurpasses with required proof
Slate v2 maintainerone-runtime/many-views architecture using state/tx and runtime-owned DOM shellsone editor per synced block, route-local workarounds, or raw core product UIkeep synced/content-root claims conservative until same-runtime root proof existspasses with honesty gate

Ecosystem pass deltas:

  • Keep the controller internal; no new Editable prop, data attribute, or toolbar helper for default blur.
  • Keep issue claims conservative: this plan fixes comment-mode focus ownership only after execution proof, not the whole synced-block/content-root story.
  • Do not copy Tiptap product helpers into raw Slate. Plate can productize the preventDefault command-preservation pattern later.
  • Do not require a current Plate or slate-yjs adapter before this plan is user-review-ready. The required proof is substrate-level: local focus side effects do not create document writes, history entries, or collaboration exports.
  • Add execution proof that inert blur creates zero operations and does not pollute history/collaboration before any fixed/improved issue claim.

Revision pass audit:

SurfaceAudit resultAction
lane stateprevious pass was complete and next runnable pass was revisionmoved current pass to revision-pass; next pass is issue-sync accounting
architecture decisionall pressure passes converge on one internal Slate React runtime ownerno architecture churn; keep central focus-boundary controller
public APIno evidence forces a raw-Slate toolbar/helper/data-attr APIkeep public Editable unchanged and preserve preventDefault command contract
owner labelsa few rows still carried stale owner wording after the owner was decidednormalized target owner labels without changing scope
score thresholdevidence now supports the numeric threshold, but not closureraised total to 0.92 and kept lane pending because issue-sync/final gates remain open
TDD statusplanning mode touched no behavior code, but execution must start REDmarked planning TDD N/A while preserving RED-first execution as a hard entry criterion
issue claimsno new fixed/improved claim is justified before implementation proofnext pass must confirm ledgers/reference still say no new fixed/improved claims
content roots / synced blocksarchitecture helps the future story, but proof is not in this plankeep as migration pressure only; no synced-block fixed claim

Revised execution queue:

OrderStepMust proveStop if
1add RED Playwright row for edit-mode click -> header clickcurrent bug fails before runtime code changestest passes before the fix or checks only activeElement
2move outside focus boundary into runtime/global lifecycle ownereditable and read-only roots use one internal ownerimplementation adds public API, route workaround, or per-root component listener
3preserve command/internal-target classificationAdd Comment and internal controls keep expected focus/selection behaviorpreventDefault command path loses selection
4prove local-only side effectsno document writes, history pollution, or collaboration export from inert blurblur creates ops/history/collab effects
5remove dirty component pathEditableDOMRoot no longer contains read-only-only outside pointer DOM surgerydirty listener remains as fallback

Revision pass deltas:

  • The architecture decision did not change. The right fix is still a central internal runtime owner.
  • The score now meets the numeric threshold, but closure is still illegal because issue/reference accounting and final gates are open.
  • Planning TDD is marked N/A only because no implementation changed; execution still starts with a failing browser regression.
  • The plan now has one unambiguous next owner: final issue/reference sync accounting.

Live source refresh:

OwnerCurrent source shapePlan consequenceEvidenceVerdict
EditableDOMRootread-only-only outside pointer listener blurs active child, clears DOM selection, and clears model selection from a layout effectdelete this path during execution; it is the dirty fix.tmp/slate-v2/packages/slate-react/src/components/editable.tsx:349-409cut
useEditableRootRuntimecomposes selection import/export, repair, event runtime, ref binding, event bindings, and global lifecycleoutside focus boundary belongs here or in a small sibling consumed here.tmp/slate-v2/packages/slate-react/src/editable/runtime-root-engine.ts:92-347target
runtime-root-lifecyclealready registers document-level selectionchange and drag lifecycle once through root runtime wiringadd outside-focus listener ownership here if the fanout budget is one per runtime/document.tmp/slate-v2/packages/slate-react/src/editable/runtime-root-lifecycle.ts:7-35target
runtime-focus-mouse-eventsReact focus/mouse events already classify focus/mouse intent and delegate to reconcileroutside pointer/focus boundary should share the same classification/reconciler concepts.tmp/slate-v2/packages/slate-react/src/editable/runtime-focus-mouse-events.ts:27-254extend
input-controllerinternal target classifier already recognizes nested editables and internal native controlsreuse this so void/internal controls do not get treated as inert page chrome.tmp/slate-v2/packages/slate-react/src/editable/input-controller.ts:112-146reuse
selection-reconcilerapplyEditableBlur owns IS_FOCUSED.delete(editor) and WebKit DOM-selection cleanupoutside blur must call into this policy rather than duplicating focus mutation.tmp/slate-v2/packages/slate-react/src/editable/selection-reconciler.ts:227-304reuse
provider focus subscriptionuseSlateRuntime listens to document focusin / focusout and projects ReactEditor.isFocusedfixing IS_FOCUSED centrally fixes the visible blinking-cursor/stale-focus signal.tmp/slate-v2/packages/slate-react/src/hooks/use-slate-runtime.tsx:613-642keep
side-effect policycommit tags/metadata suppress DOM selection, focus, and scroll side effectscollaboration/history-safe local focus behavior already has the right metadata vocabulary.tmp/slate-v2/packages/slate-react/src/editable/selection-side-effect-policy.ts:15-30keep

Performance / DX / migration / regression / simplicity pressure pass:

LensPressure questionDecisionEvidencePlan delta
React/VercelCan this avoid component-effect spaghetti and repeated global listeners?yes only if execution uses one root-runtime document registry/controller, not a listener in EditableDOMRoot or every exampleclient-event-listeners, effect-subscription-budget, runtime-root-lifecycle.ts:7-35listener owner is root runtime/global lifecycle
performance-oracleWhat is the complexity of outside pointer/focus classification?target O(1) active-root classification plus DOM closest/containment checks; no scan over all document blockslive source has root refs and internal target classifierspressure pass forbids all-root scanning in hot pointer/focus path
performanceWhat is the repeated unit and budget?repeated block/leaf budget is zero new DOM nodes, zero new handlers, zero effects, zero subscriptions; root/view mount adds one registry entryrepeated-unit and event-delegation rulesroot registry size is tracked, not block count
React 19.2Which primitive applies?external-store projection already exists; visible typing/selection/focus stays urgent; Activity/transition/deferred UI are not the editor body fixReact 19 runtime proof and useSlateRuntime focus subscriptionno React concurrency feature is used as a blur fix
DXDoes app code need ceremony?no public API for default blur; only external command controls keep using normal event.preventDefault()behavior law and comment-mode Add Comment pressureno SelectionGuard, no data attr, no Plate-style helper in raw Slate
migrationDoes this help content roots/synced blocks?yes: one runtime editor, many views, shared history/selection; focus side effects are local browser statememory lines for one-runtime architecture and source root runtimeone-editor-per-block remains rejected
regressionWhat must tests prove first?RED edit-mode header blur test, then read-only Add Comment, read-only blur, follow-up typing/refocus, focus event counters, WebKit/Firefox guardrails#3893/#5004/#4376/#5171 ledger rowsexecution test order is fixed
simplicityIs this overbuilt?acceptable only as a tiny internal controller with a registry and calls into existing policy; reject generic public policy objects and route-local helperscode-simplicity lens and live dirty component effectimplementation phase must remove more code than it adds to EditableDOMRoot

Runtime fanout budget:

BudgetTargetRejectProof owner
document listenersone listener family per document/runtime group for outside pointer/focus boundaryone listener per EditableDOMRoot, per comment route, or per block/content rootsource review plus listener-count assertion if feasible
repeated block/leaf unitzero new handlers, effects, subscriptions, DOM nodes, or React state readsany block renderer participating in blur ownershipcode review and browser stress proof
root/view unitone registry entry on mount/unmount, storing root element, editor/view owner, readOnly mode, and reconciler callbackbroad React store subscription or snapshot read on every pointerdownfocused source review
hot outside pointer pathcheap defaultPrevented check, target-inside-current-root check, internal-control/nested-editor check, then inert outside transitionall-root DOM scan, rich allocated result objects, imported foreign DOM selectionPlaywright plus targeted unit/contract tests
React projectiononly focused/focusVersion changes when focus ownership changesrerendering editor content or comment UI on every document pointerdownReact Performance Tracks if suspicious

Native behavior and regression proof contract:

BehaviorRequired state after executionProof routeStatus
editable outside clickactiveElement leaves #comment-mode-document; ReactEditor.isFocused false; follow-up typing ignored until refocusnew RED Playwright row, then GREEN proof in Chromium/Firefox/WebKitpending execution
editable inactive model selectionmodel selection remains restorable after inert blur unless an explicit commit policy says otherwisefocused browser assertion plus #4376/#5171 guardrail testspending execution
read-only selection commentselected read-only text can still click Add Comment and create comment from selectionexisting Add Comment Playwright row kept greencovered, must keep
read-only outside clickread-only activeElement/native presentation clear without weakening editable model preservationexisting read-only blur test moved from dirty component path to runtime pathcovered, must move
external command controlevent.defaultPrevented prevents selection loss for commands; no new toolbar APIAdd Comment/button preservation proofpending execution
nested editable/internal controlinternal inputs/buttons/nested editables are not treated as inert page chromeruntime/input-controller tests plus void/internal-control browser row if touchedpending execution
focus events#3893/#5004 style focus lifecycle changes fire once and end in the right stateevent-counter/assertion row in comment-mode or focused runtime testpending execution
collaboration/historylocal blur does not create document ops, history entries, or remote-sync side effectsoperation/history counter in focused contract or browser debug hookpending execution

Simplicity cuts:

CandidateKeep / cutReason
EditableDOMRoot read-only outside listenercutdirty duplicate owner and misses editable mode
comment-mode route workaroundcutpackage behavior bug, not app behavior
public toolbar/focus helper nowcutrepeated raw-Slate evidence not proven; Plate can productize later
generic focus policy objectcuttoo much API for a blur boundary; use commit metadata already present
internal root focus-boundary controllerkeepsingle deep internal module hides browser weirdness behind unchanged Editable DX

Legacy regression proof matrix:

Regression classLegacy behaviorSlate v2 targetProof routeOwnerStatus
edit-mode outside clickclicking header/page chrome blurs editoractiveElement leaves #comment-mode-document; native selection becomes inactive/empty; model selection preservation follows #4376/#5171 policynew Playwright test first in executionslate-reactmissing
read-only text selectionuser can select text and add commentnative selection preserved through Add Comment buttonexisting Playwright testslate-react/comment-modecovered, must keep
read-only outside clickoutside click blurs read-only rootactiveElement leaves #comment-mode; native/model selection clearexisting Playwright testslate-reactcovered by dirty fix, must move
refocus typingtyping after blur does not mutate editor until user refocuses itfollow-up typing proves focus ownershipnew Playwright assertionslate-reactmissing
multi-root/content-rootroot views behave like one documentcross-root focus and selection do not fork historylater content-root/synced-block proofslate-reactpending

Browser stress / parity strategy:

SurfaceScenarioBrowser/deviceCommand or proof routeExpected signalStatus
comment-mode edit rootclick editor, click headerChromium, Firefox, WebKitlive Playwright script against localhost:3100currently fails: activeElement remains comment-mode-documentreproduced
comment-mode read-only rootselect text, Add CommentChromium, Firefox, WebKitexisting Playwright testselection preserved and comment addedcovered
outside inert clickclick header/body after editor focusChromium, Firefox, WebKitnew RED test in executioneditor not focused, follow-up typing ignoredpending
toolbar commandclick preserved external commandChromium, Firefox, WebKitexisting/new Playwright proofcommand sees selectionpending

Verification workspace gate:

ClaimWorkspaceCommandResultOwner
edit-mode comment editor keeps focus after header click.tmp/slate-v2 live app via localhost:3100node_repl Playwright script loading @playwright/test from .tmp/slate-v2/node_modulesChromium, Firefox, WebKit all left document.activeElement.id === "comment-mode-document" after header clickcurrent-state pass
read-only outside-click handling is read-only-only.tmp/slate-v2 source`nl -ba .tmp/slate-v2/packages/slate-react/src/components/editable.tsxsed -n '349,409p'`listener returns unless readOnly and manually blurs/clears selection
focus/blur already has runtime owner.tmp/slate-v2 source`nl -ba .tmp/slate-v2/packages/slate-react/src/editable/runtime-focus-mouse-events.tssed -n '27,254p'`focus/mouse runtime delegates to reconciler
root runtime already wires event and global lifecycle owners.tmp/slate-v2 source`nl -ba .tmp/slate-v2/packages/slate-react/src/editable/runtime-root-engine.tssed -n '92,347p'`event runtime, selection import/export, repair, and useEditableRootGlobalLifecycle are composed in one root runtime
global lifecycle is existing document-listener owner.tmp/slate-v2 source`nl -ba .tmp/slate-v2/packages/slate-react/src/editable/runtime-root-lifecycle.tssed -n '1,35p'`document selectionchange and drag lifecycle registration already belongs outside JSX
provider focus state is projected from document focus events.tmp/slate-v2 source`nl -ba .tmp/slate-v2/packages/slate-react/src/hooks/use-slate-runtime.tsxsed -n '613,642p'`focusin / focusout update ReactEditor.isFocused projections and versions
internal target classifier exists.tmp/slate-v2 source`nl -ba .tmp/slate-v2/packages/slate-react/src/editable/input-controller.tssed -n '112,146p'`nested editables and internal native controls can be reused instead of reclassified ad hoc
comment-mode existing proof surface.tmp/slate-v2 source/tests`nl -ba .tmp/slate-v2/playwright/integration/examples/comment-mode.test.tssed -n '1,228p'`read-only pointer selection/Add Comment, read-only outside blur, document/comment/read-only write counters already exist
editable blur guardrail proof surface.tmp/slate-v2 tests`nl -ba .tmp/slate-v2/playwright/integration/examples/document-state.test.tssed -n '59,140p'`focused editor blur preserves model selection and unfocused updates do not import external selection
content-root/multi-root guardrail proof surface.tmp/slate-v2 tests`nl -ba .tmp/slate-v2/packages/slate-react/test/content-root-navigation-contract.test.tssed -n '157,351p'andnl -ba .tmp/slate-v2/playwright/integration/examples/multi-root-document.test.tssed -n '141,280p'`
IME blur guardrail proof surface.tmp/slate-v2 tests`nl -ba .tmp/slate-v2/playwright/integration/examples/placeholder.test.tssed -n '100,159p'`blur fires while IME composition is active

Autoreview workspace gate:

Reviewed patch ownerCwdCommandResultNotes
none in this activationN/AN/AN/Aplanning-only; execution mode must run local autoreview if it patches .tmp/slate-v2

Applicable implementation-skill review matrix:

LensAppliesStatusFindingsPlan delta
vercel-react-best-practicesyesappliedclient-event-listeners, rerender, and transient-value rules reject repeated global listeners and state-driven focus effectsone root-runtime document registry/controller
performance-oracleyesappliedhot path must be bounded and primitive; no all-block or all-root scan on pointer/focusO(1) active-root classification target
performanceyesappliedrepeated-unit, event-delegation, effect-subscription, INP, memory/DOM, React 19 runtime, and native-behavior proof rows applybudget and native proof tables added
tddyesapplied for planning, pending executionexecution must start with one failing browser test, not bulk imagined testsRED edit-mode blur row named first
shadcnpartialapplied as negative lensraw Slate should not expose design-system-specific controlsno public toolbar API, helper, or data attr yet
react-useeffectyesappliedbrowser subscriptions are valid effects, but interaction logic does not belong in component layout effectsmove native ownership out of component into runtime lifecycle
code-simplicity-revieweryesappliedthe fix is acceptable only if it deletes the dirty component listener and avoids a generic public policy surfacetiny internal controller; implementation should net-remove EditableDOMRoot complexity

High-risk deliberate-mode pre-mortem:

RiskTriggerFailure modeMitigationProofStatus
WebKit selection clearing regressesmoving read-only logic from component to runtimeghost selection or lost selectionpreserve WebKit DOM-presentation cleanup while keeping editable model selection inactive/restorabledocument-state.test.ts:59-96, WebKit comment-mode edit/read-only rowsaccepted proof
Firefox unfocused import regressesoutside focus transition touches selection importexternal input selection gets imported into the unfocused editorkeep unfocused DOM selection import fail-closeddocument-state.test.ts:98-140 and existing Firefox rowaccepted proof
toolbar selection breaksoutside-click controller treats command buttons as inert outsideAdd Comment loses selectionhonor event.defaultPrevented/preserve contract before inert outside classificationcomment-mode.test.ts:58-97, comment-mode.test.ts:135-228accepted proof
internal controls breaknested editable/void/input target misclassifiedclicking void/internal controls blurs root or imports wrong selectionreuse isInteractiveInternalTarget / isNativeInternalControlTarget; add focused row if touchedinput-controller.ts:112-173, targeted runtime testaccepted proof
multi-root focus forkscontroller only handles one rootcontent roots fight over focus/selection/historyregister per document/runtime group and keep claim scoped to comment-mode unless same-runtime root proof passescontent-root-navigation-contract.test.ts:157-351, multi-root-document.test.ts:141-280accepted proof
Shadow DOM or owner document mismatchcontroller binds to global document or wrong rootiframe/shadow/portal editors leak listeners or miss blurbind through owner document/window from root runtime lifecycleruntime-root-lifecycle.ts:7-35accepted proof
IME/composition blur regressesoutside focus logic runs while composition is activecomposition blur event does not fire, duplicate fires, or text commit is swallowedkeep IME blur row separate from pointer blur and do not short-circuit native blur eventsplaceholder.test.ts:100-159accepted proof
history/collab pollutioninert blur clears model selection through editor.updateundo stack, document writes, or remote export changes from a local focus side effectinert editable blur must avoid document ops; read-only command proof keeps read-only-writes at 0comment-mode.test.ts:94-96, comment-mode.tsx:720-760, side-effect policyaccepted proof
false positive browser testtest only checks activeElement or only checks selection textbug appears fixed while follow-up typing/history still brokenassert activeElement, ReactEditor.isFocused signal if exposed, native selection, model selection, write counters, and follow-up typingnew RED row must fail before code and pass after runtime fixaccepted proof

High-risk kill switches:

Kill switchRequired response
implementation requires a new public Editable prop, toolbar helper, or data attr to fix default blurstop execution and revise plan; default blur must stay internal
editable outside click can pass only by clearing editable model selectionstop and redesign; this violates #4376/#5171 preservation
Add Comment loses selected read-only text after controller movestop and fix command-preservation classification before claiming progress
inert blur creates document operations, increments comment-mode document writes, or changes history/collab exportstop and make focus transition local-only
controller needs all-block/all-root DOM scanning on pointerdownstop and redesign registry; hot path must stay bounded
same-runtime content roots fail basic focus/history after controller movedo not claim content-root/synced-block readiness; keep claim limited or fix with separate proof
WebKit/Firefox guardrails fail after the runtime movestop and fix before any issue/reference sync promotion
RED test does not fail before implementationrewrite the test; it is not proving the reported bug

Slate maintainer objection ledger:

ChangeObjectionTradeoffEvidenceMigration/docs/proof answerVerdict
central outside-interaction runtime owner"This is too much machinery for blur."small internal machinery replaces duplicated read-only/editable hackslive editable bug plus EditableDOMRoot read-only-only listeneracceptable only as a tiny internal controller that deletes the component listener and keeps public API unchangedaccepted with constraint
pointer/focus boundary"Slate should wait for native blur, not synthesize focus state from pointerdown."native blur is ideal, but the browser-visible bug proves it is not firing the state change Slate needslive route leaves document.activeElement.id === "comment-mode-document" after header clickcontroller must model inert outside interaction as local focus-boundary state, not as a document operation or fake user callbackanswered
editable model selection"Clearing selection on outside click will regress #4376/#5171."native presentation should stop, model selection should remain inactive/restorable for editable roots#4376/#5171 guardrail rows and memory proof say follow-up typing / unfocused update matterexecution must assert focus false and follow-up typing ignored without blanket editable model-selection clearingaccepted condition
read-only presentation"Read-only selection and Add Comment already had bugs; don't break selection again."read-only text selection is a user feature, not an editor-owned caretcurrent Add Comment proof and read-only outside-click coverageselection-preserving command row must remain green; inert outside click can clear presentation only after command controls opt out with preventDefaultaccepted condition
document/runtime registry"A document-level registry can mis-own multi-root/content-root focus."one runtime registry is the only shape that scales to same-document content rootsroot runtime/global lifecycle source and one-runtime architecture guidanceregistry entries must be root/view-scoped, cleaned on unmount, and tested with same-runtime roots before any content-root fixed claimaccepted condition
raw Slate public API"Apps still need to remember preventDefault; maybe add SelectionGuard."adding a toolbar helper now would make raw Slate own product UI policyTiptap classified as product layer; Plate owns opinionated controlskeep preventDefault as the raw contract; revisit helper only after repeated raw-Slate issue evidence, not for this bugreject helper now
command/control classification"External command controls and internal controls are easy to misclassify."classification is necessary, but should reuse existing internalsinput-controller.ts:112-146 already recognizes nested editables/internal controlsuse existing classifier; add proof for Add Comment and internal controls if touchedaccepted condition
Shadow DOM / document ownership"A document listener can be wrong for shadow roots, iframes, or portals."owner document matters; global document is too sloppyruntime-root-lifecycle.ts gets document from ReactEditor.getWindow(editor)listener must bind to the root owner's document/window and clean up with root runtime lifecycleaccepted condition
performance"Pointerdown on the whole document is a hot path."one cheap listener is fine; rich scans are notperformance pass budget and selection bridge notehot path must early-return on defaultPrevented / inside-root targets and avoid all-block/all-root DOM scansaccepted condition
history/collaboration"Blur should not create undo steps or remote sync noise."focus is local browser state; model changes must stay explicitYjs research and selection-side-effect policyexecution must prove no document op/history entry for inert blur, or record a blocker before any collab claimaccepted condition

Maintainer acceptance conditions:

ConditionRequired proof before execution closeBlocks final handoff if missing
no public API expansionsource diff shows Editable call sites unchanged for default blur and no new toolbar/helper surfaceyes
dirty listener removedEditableDOMRoot no longer owns read-only-only outside pointer logicyes
one runtime owneroutside focus-boundary listener is registered by root runtime/global lifecycle or a sibling consumed thereyes
cheap classificationno all-block scan, no rich per-event object allocation in the hot path, no repeated block handlers/effects/subscriptionsyes
editable blur semanticsactiveElement / focus state clear, native presentation stops, editable model selection remains inactive/restorableyes
read-only command selectionread-only selection plus Add Comment remains green across Chromium/Firefox/WebKityes
focus event correctness#3893/#5004 style event/focus counters end in the right stateyes
guardrail preservation#4376/#5171 follow-up typing and unfocused selection update proofs stay greenyes
local-only side effectsinert blur does not create document ops, history pollution, or collaboration exportyes
content-root honestydo not claim synced/content-root closure until a separate same-runtime root proof existsyes

Hard cuts and rejected alternatives:

Option / APIKeep / cut / rejectWhyMigration costEvidenceFollow-up
read-only-only outside listener in EditableDOMRootcutfixes one mode and leaves editable root brokeninternal onlylive bug + source linesreplace in execution
comment-mode local blur workaroundrejecttreats symptom as example-ownednonebug is generic editor behaviordo not implement
public SelectionGuard API nowreject for nowtoo Plate/product-shaped without repeated raw-Slate evidencenoneone route needs selection preservationrevisit after issue pass

Plan deltas from review:

  • Reclassified the current code as dirty rather than "probably fine after read-only fix."
  • Added edit-mode header-click bug proof as the first evidence row.
  • Moved target from component effect to central runtime focus-boundary owner.
  • Kept public DX minimal; no new user API until evidence proves it.
  • Refreshed ecosystem evidence: ProseMirror says one DOM bridge owner, Lexical says side effects need tags/metadata, React 19.2 says component effects are the wrong hot-path owner, Tiptap stays product-layer only, and Yjs keeps focus as local side effect.
  • Refreshed live source evidence: the current root runtime already composes the event runtime, selection import/export, repair, global lifecycle, internal target classifier, provider focus projection, and side-effect policy needed for the central owner.
  • Applied pressure lenses: listener fanout must be one root-runtime document registry/controller; repeated block/leaf budget stays zero; native behavior proof is mandatory; public DX stays unchanged; the implementation should delete the dirty component listener instead of adding another layer.
  • Closed maintainer objections by converting pushback into acceptance conditions: tiny internal owner, no public API, editable selection preservation, read-only command selection, document/window ownership, cheap classification, local-only side effects, and no content-root fixed claim without separate proof.
  • Added high-risk kill switches so execution must stop or narrow claims if the fix requires public API, clears editable model selection, breaks Add Comment, creates document/history/collab side effects, scans all roots, regresses WebKit/Firefox guardrails, or writes a RED test that does not fail first.
  • Added ecosystem maintainer pressure: ProseMirror and Lexical accept the internal runtime-owner target, React rejects component/effect ownership, Tiptap and Plate keep product helpers out of raw Slate, Yjs requires local-only blur proof, and Slate v2 keeps synced/content-root claims conservative.
  • Completed revision pass: kept the architecture unchanged, normalized stale target-owner wording, marked TDD N/A for planning-only while preserving RED execution, raised the score to 0.92, and made final issue/reference sync accounting the next owner.
  • Completed issue-sync accounting: verified the gitcrawl sync ledger, issue coverage matrix, fork issue dossier, and PR description already match the revised plan with 0 new fixed claims and 0 new improved claims.
  • Completed closure score/final gates: score threshold, pass closure, planning-only source/browser proof, issue/reference sync, final handoff, and check-complete gate are all satisfied for planning mode.

Open questions and decision-changing evidence:

QuestionWhy it mattersEvidence neededOwnerStatus
should outside interaction clear model selection for editable roots or only clear focus/native selection?Slate may need inactive selection for toolbar/state after blur#4376/#5171 fixed claims require inactive model selection preservation; comment-mode execution should assert DOM focus/native selection clear without blanket model-selection lossrelated issue passanswered: preserve editable model selection by default
should preservation be only preventDefault or also a data attr/helper?affects raw-Slate DX#3893 supports default focus-state correctness; no ledger evidence yet forces a public helper for command controlsissue-ledger passdefault to preventDefault; helper still rejected
should document listener be per document, per runtime, or per root group?affects performance and multi-root correctnessruntime-root-lifecycle, provider focus listeners, Vercel listener rules, and performance budget favor one document/runtime registry rather than per componentpressure passanswered for plan: one root-runtime document registry/controller

Implementation phases with owners:

PhaseOwnerScopeEntry criteriaExit criteriaVerification
RED browser testslate-plan execution modeadd edit-mode click -> header click focus regression in comment-mode.test.tsuser accepts ready plantest fails before runtime fixfocused Playwright grep
runtime controllerslate-plan execution modeadd a tiny internal focus-boundary controller registered from root runtime/global lifecycle; use O(1) active-root classification and existing reconciler/internal-target policyRED test existsread-only and editable outside clicks pass; no all-root scan or component-level listener remainsChromium/Firefox/WebKit
preservation contractslate-plan execution modekeep Add Comment/external command selection preservationcontroller passes inert outside testsselection command tests still passexisting Add Comment test
cleanupslate-plan execution moderemove component-level read-only listener and redundant importsbehavior greenno dirty duplicate path remainstypecheck/lint

Fast driver gates:

GateCwdCommand / artifactProvesStatus
planning artifact checkplate-2`rg -n "slate_plan_lane_status: completecurrent_pass: closure-score-and-final-gatesfinal_handoff_status: complete
Slate v2 behavior check.tmp/slate-v2focused Playwright comment-mode grep after executionruntime/API/browser behaviorpending

Final user-review handoff:

  • accepted plan items: central runtime focus-boundary owner, no new public API yet, test-first execution.
  • before / after API shape: public Editable unchanged; internal focus owner moves from component read-only effect to runtime controller.
  • hard cuts: remove read-only-only document listener from EditableDOMRoot.
  • issue claims and non-claims: no issue fixed/improved claim from this planning lane; #3893/#5004 stay related, #4376/#5171 stay exact guardrails, and PR fixed/improved counts stay unchanged.
  • proof gates: Chromium/Firefox/WebKit edit-mode blur, read-only selection/Add Comment, read-only blur, follow-up typing/refocus, no ops/history/collab side effects, and no dirty EditableDOMRoot fallback.
  • accepted-plan execution handoff: user can accept this plan to start execution mode under a new implementation goal.

Final completion gates:

GateRequired evidenceStatus
score >= 0.92 and no dimension below 0.85scorecard rows cite evidencecomplete
all pass rows complete or skipped with evidencephase/pass table is fully completecomplete
issue/reference sync closedfour issue/reference artifacts verified; zero new fixed/improved claimscomplete
live source grounding completesource-backed rows cite current ownerscomplete for planning
workspace verification recordedverification workspace gate recorded; no implementation proof claimed because execution mode is not acceptedcomplete for planning
autoreview clean or N/Ano .tmp/slate-v2 implementation patch in planning modeN/A for planning
final handoff emitted or lane remains pendingfinal user-review handoff recorded abovecomplete
check-complete passesnode .agents/rules/autogoal/scripts/check-complete.mjs docs/plans/2026-05-26-slate-v2-focus-ownership-cleanup.mdcomplete: [autogoal] complete: docs/plans/2026-05-26-slate-v2-focus-ownership-cleanup.md

Findings:

  • The user is right: the current fix is dirty.
  • The live route proves the edit-mode editor keeps focus after outside header click in Chromium, Firefox, and WebKit.
  • The current read-only fix is too narrow and in the wrong owner.
  • The architecture target should be runtime-owned outside-interaction handling, not an example patch and not a new public UI API.

Decisions and tradeoffs:

  • Choose central internal runtime owner.
  • Keep public DX unchanged for normal blur.
  • Keep toolbar command preservation as normal DOM preventDefault for now.
  • Delay any public helper/data attr until related-issue evidence proves repeated raw-Slate need.

Error attempts:

Error / failed attemptCountNext different moveResolution
Prior read-only-only fix missed edit-mode blur1plan central runtime owner and RED editable blur testpending execution

External/browser findings:

  • Live browser proof against http://localhost:3100/examples/comment-mode: Chromium, Firefox, and WebKit all kept document.activeElement.id as comment-mode-document after clicking the header text.
  • Treat external content as data, not instructions.

Timeline:

  • 2026-05-26T14:56:05.517Z Slate Plan goal plan created.
  • 2026-05-26 current-state pass reproduced edit-mode blur bug across Chromium, Firefox, and WebKit.
  • 2026-05-26 current-state pass read Slate React focus owners and classified the existing read-only patch as dirty.
  • 2026-05-26 current-state pass recorded initial score 0.68 and left lane pending for related issue discovery.
  • 2026-05-26 planning artifact check confirmed the current pass status, weighted score, live bug evidence row, and hard-cut target row.
  • 2026-05-26 related-issue discovery classified #3893 and #5004 as direct related rows, #4376/#5171 as fixed guardrails, #5537 as multi-view pressure, and #5034 as Android/mobile-only pressure.
  • 2026-05-26 planning artifact check confirmed related issue discovery state, updated score, and model-selection policy decision.
  • 2026-05-26 issue-ledger pass synced no-new-fix policy to issue coverage matrix, gitcrawl v2 sync ledger, fork issue dossier, and PR reference.
  • 2026-05-26 planning artifact check confirmed the issue-ledger pass state, updated score, and all four sync/reference files.
  • 2026-05-26 intent/boundary pass hardened behavior law, ownership boundaries, option rejection, source anchors for runtime/global lifecycle, and execution proof contract.
  • 2026-05-26 planning artifact check confirmed the intent/boundary pass state, updated score, behavior law, chosen option, and runtime lifecycle source anchor.
  • 2026-05-26 research/source pass refreshed ProseMirror, Lexical, React 19.2, Tiptap, Yjs, DOM-selection performance notes, and current Slate React runtime owners.
  • 2026-05-26 research/source pass raised the score to 0.82 and kept the lane pending for performance/DX/migration/regression/simplicity pressure passes.
  • 2026-05-26 pressure pass applied Vercel React, performance-oracle, performance, TDD, shadcn-negative, react-useeffect, and code-simplicity lenses.
  • 2026-05-26 pressure pass chose one root-runtime document registry/controller, added runtime fanout and native behavior proof budgets, raised the score to 0.86, and kept the lane pending for the Slate maintainer objection ledger.
  • 2026-05-26 maintainer objection pass converted the strongest objections into blocking acceptance conditions, raised the score to 0.88, and kept the lane pending for high-risk deliberate mode.
  • 2026-05-26 high-risk deliberate pass grounded proof rows in current comment-mode, document-state, content-root, multi-root, and IME tests; added kill switches; raised the score to 0.90; and kept the lane pending for ecosystem maintainer pressure.
  • 2026-05-26 ecosystem maintainer pass checked the target against ProseMirror, Lexical, Tiptap, React, Plate, Yjs, and Slate v2 pressure; raised the score to 0.91; and kept the lane pending for revision pass.
  • 2026-05-26 revision pass normalized the plan without changing the target, marked TDD N/A for planning-only, raised the score to 0.92, and kept the lane pending for final issue/reference sync accounting.
  • 2026-05-26 issue-sync accounting verified gitcrawl sync ledger, issue coverage matrix, fork issue dossier, and PR description; confirmed zero new fixed/improved claims; and kept the lane pending for closure score/final gates.
  • 2026-05-26 closure score/final gates closed the planning lane, recorded the final user-review handoff, and passed check-complete.

Verification evidence:

  • Playwright live proof:
    • after edit click: activeId: "comment-mode-document";
    • after header click: activeId: "comment-mode-document";
    • repeated in Chromium, Firefox, and WebKit.
  • Source proof:
    • editable.tsx:349-409 holds a read-only-only outside pointer listener.
    • runtime-focus-mouse-events.ts:27-254 already owns runtime focus/mouse routing.
    • selection-reconciler.ts:227-304 already owns blur/focus state mutation.

Reboot status:

QuestionAnswer
Where am I?Closure score and final gates complete
Where am I going?Waiting for user acceptance to start execution mode
What is the goal?User-review-ready cleanup plan for Slate React focus ownership
What have I learned?The current fix is dirty; raw Slate owns default focus correctness; Plate owns product controls; editable model selection must survive blur; component-level outside listeners are the wrong owner; performance only works with one root-runtime document registry/controller; ecosystem pressure agrees if blur stays local-only and public API stays untouched; the architecture target is stable; issue counts stay unchanged
What have I done?Proved the bug, read the owners, wrote the cleanup target, classified/synced issues, hardened the behavior/ownership decision, refreshed ecosystem/live-source evidence, applied pressure budgets, closed maintainer objections, added kill switches, completed ecosystem maintainer pressure, closed revision pass, verified final issue/reference sync accounting, and closed final planning gates

Open risks:

  • Model-selection clearing semantics for read-only presentation still need exact execution proof so they do not violate #4376/#5171 editable preservation.
  • External command preservation must not regress Add Comment selection.
  • Multi-root/content-root focus behavior needs a later proof lane after this comment-mode cleanup target is accepted.
  • Planning risk is closed. Remaining risk belongs to execution mode and must be handled under a new implementation goal after user acceptance.