docs/plans/2026-06-15-4951-block-placeholder-list-item.md
Objective: Fix #4951 block placeholder list-item bug; done when repro fails before fix, passes after fix, checks/review pass, and PR/sync-back complete.
Goal plan: docs/plans/2026-06-15-4951-block-placeholder-list-item.md
Template: docs/plans/templates/task.md
Primary template: docs/plans/templates/task.md
Applied packs:
Task source:
Completion threshold:
node .agents/skills/autogoal/scripts/check-complete.mjs docs/plans/2026-06-15-4951-block-placeholder-list-item.md passes.Verification surface:
pnpm --filter @platejs/core build && bun test packages/core/src/lib/plugins/slate-extension/SlateExtensionPlugin.spec.tsx packages/utils/src/react/plugins/BlockPlaceholderPlugin.spec.tsxpnpm turbo typecheck --filter=./packages/core --filter=./packages/utilspnpm --filter www build:sourcepnpm lint:fixpnpm check before PR updateConstraints:
Boundaries:
BlockPlaceholderPlugin source/tests, repo instructions.packages/core state-empty API/tests, packages/utils placeholder plugin/tests, affected docs, .changeset, this goal plan, PR/issue text.Output budget strategy:
rg patterns, targeted test commands, and capped command output. Avoid broad repo scans and generated/build output streams.Blocked condition:
Task state:
Current verdict:
node.isMetadataProp, has NodeIdPlugin claim its configured id key as inert metadata, and makes BlockPlaceholderPlugin delegate its default pristine-empty policy to editor.api.isElementStateEmpty.Pre-solution issue challenge:
BlockPlaceholderPlugin hides placeholder when the only block is an empty paragraph with indent-list metadata._target remains null for a single empty list-style paragraph.BlockPlaceholderPlugin should distinguish pristine editor state from user-created structural empty blocks without list-specific caller patches, via plugin-owned pristine-empty policy.Completion rule:
update_goal(status: complete) while any required checklist item
remains unchecked. If an item does not apply, check it and add N/A: <reason>.update_goal(status: complete) until every completion threshold
above is satisfied, final handoff evidence is recorded, and
node .agents/skills/autogoal/scripts/check-complete.mjs docs/plans/2026-06-15-4951-block-placeholder-list-item.md passes.Start Gates:
| Gate | Applies | Evidence |
|---|---|---|
| Skill analysis before edits | yes | Read task/autoreview/tdd/autogoal skills; using task plus pre-solution challenge, red-green TDD, and goal plan. |
| Active goal checked or created | yes | Initial run used the goal plan. Follow-up rename run created goal Hard rename node.isPropEmpty to node.isMetadataProp... and reused this ledger. |
| Source of truth read before edits | yes | gh issue view 4951 --comments --json ... read issue body/comments. |
| Tracker comments and attachments read | yes | Issue has no comments or attachments. |
| Video transcript evidence required | no | N/A: issue contains no video/screen recording evidence. |
| Pre-solution issue challenge required | yes | Public issue has bug claim, diagnosis, and proposed fix; challenge recorded in this plan. |
| Reproduction verdict before implementation | yes | Reproduced by failing bun test packages/utils/src/react/plugins/BlockPlaceholderPlugin.spec.tsx -t "keeps the target on a single empty list item" before implementation. |
| Repro escalation ladder selected | yes | Start with focused plugin spec; higher browser levels N/A unless tests cannot model behavior. |
| Suggested fix reviewed against durable boundary | yes | Rejected list-specific hardcode as too narrow; durable boundary is generic pristine-state distinction. |
docs/solutions checked for non-trivial existing-code work | no | N/A: narrow plugin regression; existing source/tests are the authority. |
| TDD decision before behavior change or bug fix | yes | Use red regression test in BlockPlaceholderPlugin.spec.tsx before implementation. |
| Branch decision for code-changing task | yes | Created dedicated branch codex/4951-block-placeholder-list from main. |
| Release artifact decision | yes | .changeset required because @platejs/core public API and @platejs/utils published package behavior changed. |
| Browser tool decision for browser surface | yes | Required by repo policy for package/content changes; Browser loaded http://localhost:3050/blocks/block-placeholder-demo, force-focused the editor, but could not move Slate selection into the empty paragraph and found no editor global for direct plugin-state inspection. |
| PR expectation decision | yes | Task workflow expects verified PR before tracker sync. |
| Tracker sync expectation decision | yes | Comment on #4951 after PR exists and verification is complete. |
| Output budget strategy recorded | yes | Exact source reads and capped focused commands only. |
| Package/API pack selected | yes | --with package-api applied because published package runtime behavior changes. |
| Public surface or package boundary identified | yes | @platejs/core editor/plugin API plus @platejs/utils runtime behavior via BlockPlaceholderPlugin. |
| Release artifact path selected | yes | .changeset for @platejs/core and @platejs/utils. |
changeset skill loaded when .changeset is required | yes | Read .agents/skills/changeset/SKILL.md and .agents/rules/changeset.mdc; added separate .changeset/core-node-metadata-prop.md and .changeset/utils-block-placeholder-list.md. |
| Barrel/export impact decision recorded | no | N/A: no exports or exported file layout expected. |
Work Checklist:
<video-transcripts> XML, or marked N/A with reason.valid, not reproduced, invalid,
wont-fix, partially valid, or platform limitation. Feature, docs,
support, or cleanup requests with no bug claim may mark reproduction
N/A with reason.[@Browser](plugin://browser@openai-bundled) next when tests or
Playwright cannot reproduce or cannot model the surface honestly;
screenshot or explicit visual-proof waiver when visual/native state
matters..agents/**, .claude/**,
.codex/**, skills, hooks, commands, prompts, or user-action tooling..changeset, registry changelog, or explicit no-artifact reason..changeset work loads changeset and follows its package/version/prose rules.registry-changelog pack instead of adding a package changeset.main.Completion Gates:
| Gate | Applies | Required action | Evidence |
|---|---|---|---|
| Named verification threshold | yes | Run the command, proof, source audit, or artifact check named in this plan | Red repro failed before fix; hard rename source audit found no source-owned isPropEmpty refs; focused core+utils spec, core+utils typecheck, docs source build, lint-fix, Browser route attempt, pnpm check, and final autoreview passed. |
| Pre-solution issue challenge verdict | yes | Record reporter claim, suggested fix, repro verdict, validity verdict, durable boundary, and hard-stop/pivot decision before implementation | Valid. Reproduced before implementation; rejected list-only proposed fix as too narrow. |
| Repro escalation ladder | yes | For bug/behavior claims, record test/source-level, Playwright, Browser, and screenshot/visual-proof outcomes or N/A/blocker reasons before not reproduced | Focused source-level spec reproduced and verified the behavior. Playwright/Browser/screenshot N/A because no browser-only state remained. |
| Bug reproduced before fix | yes | Record failing test/repro or N/A with reason | bun test packages/utils/src/react/plugins/BlockPlaceholderPlugin.spec.tsx -t "keeps the target on a single empty list item" failed before implementation: expected placeholder _target, received null. |
| Targeted behavior verification | yes | Run focused test/proof for changed behavior or record N/A | pnpm --filter @platejs/core build && bun test packages/core/src/lib/plugins/slate-extension/SlateExtensionPlugin.spec.tsx packages/utils/src/react/plugins/BlockPlaceholderPlugin.spec.tsx passed: 30 tests, including editor API, node.isMetadataProp, configured NodeId metadata, custom inert props, and empty list-item placeholder target. |
| TypeScript or typed config changed | yes | Run relevant typecheck | pnpm turbo typecheck --filter=./packages/core --filter=./packages/utils passed. |
| Package exports or file layout changed | no | Run pnpm brl before final verification and keep generated barrel updates | N/A: no exports or file layout changed. |
| Package manifests, lockfile, or install graph changed | no | Run pnpm install and relevant package checks | N/A: no manifests, lockfile, or install graph changed. |
| Agent rules or skills changed | no | Run pnpm install and verify generated skill sync | N/A: no agent rules or skills changed. |
| Workspace authority proof | yes | Run verification in the owning repo/package/app/route/tool and record cwd; do not count the wrong workspace as proof | Commands ran in /Users/zbeyens/git/plate; owning package proof is packages/utils. |
| Browser surface changed | yes | Capture Browser Use proof or record explicit waiver/blocker | Browser loaded http://localhost:3050/blocks/block-placeholder-demo; demo source uses BlockPlaceholderKit; Browser could force-focus the editor, but click/key automation did not move selection into the empty paragraph and no window editor global exists, so visual placeholder proof is blocked. |
| Browser final proof | yes | Attach screenshot or exact browser verification caveat when browser proof applies | Exact caveat recorded: Browser could not establish the empty-block Slate selection needed to activate the placeholder; focused React plugin spec proves _target and injected placeholder props instead. |
| CI-controlled template output changed | no | Restore generated template output or record why it is intentionally kept | N/A: no templates/** output touched. |
| Package behavior or public API changed | yes | Add a changeset or record why no changeset applies | Added .changeset/core-node-metadata-prop.md for @platejs/core and .changeset/utils-block-placeholder-list.md for @platejs/utils. |
| User-visible registry output changed | no | Use the registry-changelog pack: add/update apps/www/src/registry/changelog/entries/*.mdx, run node tooling/scripts/generate-ui-changelog-entries.mjs --write, run node tooling/scripts/generate-ui-changelog-entries.mjs --check, or record N/A | N/A: no apps/www/src/registry/** behavior output changed. |
| Docs or content changed | yes | For docs-heavy work, use --template docs; for incidental docs, verify source-backed claims, links, examples, and rendered output or record N/A | Incidental docs update to core API and block-placeholder EN/CN visibility/API rule; pnpm --filter www build:source passed and Browser route loaded. |
| High-risk mini gate | yes | For public API/runtime/package-boundary/browser/agent-action/command-contract changes, record realistic failure mode, proof plan, and why the chosen boundary is right; otherwise N/A | Failure mode: internal ids could trigger placeholders on pristine docs or the empty-state API could be owned by the wrong plugin namespace. Proof: metadata-only id test stays suppressed when NodeIdPlugin is active, configured id key is honored, custom plugins can claim inert props through node.isMetadataProp, and editor.api.isElementStateEmpty is covered. Boundary is node-plugin-owned prop inertness plus editor-facing state check plus placeholder-owned default policy. |
| Agent-native review for agent/tooling changes | no | For .agents/**, .claude/**, .codex/**, skills, hooks, commands, prompts, or user-action tooling, load .agents/skills/agent-native-reviewer/SKILL.md and close accepted/actionable findings, or record N/A | N/A: no agent/tooling surfaces changed. |
| Local install corruption suspected | no | Run pnpm run reinstall once, rerun the exact failing command, or record N/A | N/A: no suspicious env-rot failure; full check passed. |
| Autoreview for non-trivial implementation changes | yes | Load .agents/skills/autoreview/SKILL.md; use dirty local --mode local, branch/PR --mode branch --base <base>, or committed slice --mode commit --commit <ref> until no accepted/actionable findings, or record N/A for docs-only/trivial/no local patch | Final .agents/skills/autoreview/scripts/autoreview --mode local exited 0: no accepted/actionable findings; reviewer explicitly validated consistent rename to node.isMetadataProp. |
| PR create or update | yes | Run check before PR work and sync PR body to the task-style final handoff | pnpm check passed before PR update; PR #5029 body updated with node.isMetadataProp, stale-symbol audit, browser caveat, and verification. |
| Task-style PR body verified | yes | Verify the PR body with gh pr view --json body; it must preserve auto-release blocks when applicable, must not include a current-PR self-link, and must use the kitcn PR #270 emoji format: ๐ Fixes ..., ๐ข 95-100% confidence, Phase / ๐งช Tests / ๐ Browser table, and bold emoji Outcome/Caveat/Design/Verified sections | gh pr view 5029 --repo udecode/plate --json body --jq .body verified auto-release block, issue line, confidence, phase table, Outcome/Caveat/Design/Verified sections, node.isMetadataProp, no isPropEmpty, and no current-PR self-link. |
| PR proof image hosting | no | If PR body needs browser proof, replace local image paths with hosted GitHub URLs or record N/A | N/A: PR body has no browser image proof. |
| Tracker sync-back | yes | Post concise issue/Linear sync after PR exists, or record N/A/blocker | Posted https://github.com/udecode/plate/issues/4951#issuecomment-4712148949. |
| Final handoff contract | yes | Fill the final handoff fields below with exact PR/issue/confidence/tests/browser/outcome/caveats/design/verification content or N/A reason | Final handoff fields filled below. |
| Final lint | yes | Run pnpm lint:fix or scoped equivalent | pnpm lint:fix passed, no fixes applied. |
| Output budget discipline | yes | Verify no unbounded high-volume command output was streamed, or record the accidental output and recovery | One broad rg hit generated docs JSON and produced noisy output; recovered with scoped source-owned stale-symbol search excluding generated/changelog blobs. |
| Goal plan complete | yes | Run node .agents/skills/autogoal/scripts/check-complete.mjs docs/plans/2026-06-15-4951-block-placeholder-list-item.md | node .agents/skills/autogoal/scripts/check-complete.mjs docs/plans/2026-06-15-4951-block-placeholder-list-item.md passed. |
| Public API / package boundary proof | yes | Source-audit public API, exports, and package boundary impact | Public API in @platejs/core and runtime behavior in @platejs/utils changed; no exports or file layout changed; source audit via git diff, focused tests, and package typecheck passed. |
| Release artifact classification | yes | Record whether the change is published package behavior/API/types/config/runtime, registry-only, or no published user-visible delta | Published public API/runtime delta for @platejs/core and published runtime behavior delta for @platejs/utils; patch changesets added. |
| Published package changeset | yes | If published package users see a delta, load changeset, add/update one .changeset/*.md per package, and prove no forbidden minor on @platejs/slate, @platejs/core, or platejs | .changeset/core-node-metadata-prop.md has "@platejs/core": patch; .changeset/utils-block-placeholder-list.md has "@platejs/utils": patch; no forbidden core package minor. |
| Registry changelog | no | If the change is registry-only under apps/www/src/registry/**, use the registry-changelog pack and do not add a package changeset | N/A: not registry-only. |
| No release artifact | no | If no artifact is needed, record the exact reason: internal-only, docs-only, agent-only, test-only, or no user-visible delta from main | N/A: changeset is required and present. |
| Package typecheck/build/test | yes | Run owning package checks or record N/A with reason | pnpm --filter @platejs/core build && bun test packages/core/src/lib/plugins/slate-extension/SlateExtensionPlugin.spec.tsx packages/utils/src/react/plugins/BlockPlaceholderPlugin.spec.tsx, pnpm turbo typecheck --filter=./packages/core --filter=./packages/utils, and pnpm check passed. |
| Barrel/export generation | no | Run pnpm brl when exports or exported file layout changed, otherwise N/A | N/A: no exports or exported file layout changed. |
Phase / pass table:
| Phase | Status | Evidence | Next |
|---|---|---|---|
| Intake and source read | complete | issue/source/skills read; plan created and red repro failed as expected | implementation |
| Implementation | complete | node.isMetadataProp and editor.api.isElementStateEmpty added; NodeIdPlugin claims id metadata; BlockPlaceholderPlugin default delegates to the editor API; regression tests/docs/changesets added. | verification |
| Verification | complete | Focused core+utils spec, stale-symbol source audit, package typecheck, docs source build, lint-fix, pnpm check, and final autoreview passed. | PR / tracker sync |
| PR / tracker sync | complete | PR #5029 opened, issue #4951 sync comment posted, and PR body updated/read back. | closeout |
| Closeout | complete | Plan ready for mechanical completion check, amend, push. | final response |
Findings:
_target staying null for a single empty paragraph with indent: 1 and listStyleType: 'disc'.Decisions and tradeoffs:
listStyleType / indent as the fix. Treat only type and plugin-claimed inert metadata as empty; every unclaimed prop means the element carries state.node.isMetadataProp. BlockPlaceholderPlugin consumes editor.api.isElementStateEmpty directly instead of owning list or node-id semantics.node.isPropEmpty to node.isMetadataProp with no alias because the old name only existed in this unmerged PR and lied about the contract.Implementation notes:
node.isMetadataProp to plugin node config.NodeIdPlugin marks its configured idKey as inert metadata.editor.api.isElementStateEmpty(element) uses NodeApi.extractProps, ignores type, asks node.isMetadataProp plugins about the remaining props, and treats any unclaimed prop as non-empty state.Review fixes:
editor.api.slateExtension.isElementStateEmpty shape after user feedback. The durable API is node.isMetadataProp for plugin ownership and editor.api.isElementStateEmpty for callers.Error attempts:
| Error / failed attempt | Count | Next different move | Resolution |
|---|---|---|---|
Ran focused tests before workspace dependency build finished, causing missing @platejs/slate resolution | 1 | Rerun after package typecheck built workspace deps | Sequential rerun passed: 30 tests, 0 failures. |
Autoreview treated the removed editor.api.slateExtension.isElementStateEmpty shape as published API | 1 | Check origin/main for the API before accepting the finding | Rejected as false-positive: the alias only existed inside this unmerged PR and is absent from origin/main. |
| Broad stale-symbol search streamed generated docs JSON | 1 | Scope source audit to source-owned files and exclude generated/changelog blobs | Scoped rg -n "isPropEmpty" audit returned no source-owned matches. |
Verification evidence:
bun test packages/utils/src/react/plugins/BlockPlaceholderPlugin.spec.tsx -t "keeps the target on a single empty list item" failed before implementation with expected _target object vs received null.rg -n "isPropEmpty" . --glob '!docs/plans/**' --glob '!apps/www/public/rd/**' --glob '!apps/www/src/generated/**' --glob '!**/CHANGELOG.md' --glob '!node_modules/**' --glob '!**/.next/**' --glob '!**/.turbo/**' found no source-owned matches.pnpm --filter @platejs/core build && bun test packages/core/src/lib/plugins/slate-extension/SlateExtensionPlugin.spec.tsx packages/utils/src/react/plugins/BlockPlaceholderPlugin.spec.tsx passed: 30 tests, 0 failures.pnpm turbo typecheck --filter=./packages/core --filter=./packages/utils passed.pnpm --filter www build:source && pnpm lint:fix passed.pnpm check passed with exit 0; existing sidebar hook warning and multiple-core test message were non-fatal..agents/skills/autoreview/scripts/autoreview --mode local exited 0 with no accepted/actionable findings.Final handoff contract:
node.isMetadataProp and editor.api.isElementStateEmpty; NodeIdPlugin claims id metadata; BlockPlaceholderPlugin shows placeholders for empty list-state blocks while keeping pristine and metadata-only empty blocks suppressed.pnpm check passed before PR.node.isMetadataProp; @platejs/core owns the generic element state-empty check; @platejs/utils BlockPlaceholderPlugin owns when that predicate suppresses placeholders.pnpm check.gh pr view 5029 --json body verified required task-style body, node.isMetadataProp, and auto-release block.Task-style PR body contract:
<!-- auto-release:start --> block. If a changeset is
part of the diff and repo policy expects auto release, include that block.๐ Fixes #123 or ๐ Fixes โ N/A, then
an emoji confidence line like ๐ข 95-100% confidence.| Phase | ๐งช Tests | ๐ Browser |.Reproduced and Verified rows. Mark passing proof with ๐ข, repro or
failing proof with ๐ด, and non-applicable cells with โ N/A.**โ
Outcome**, **โ ๏ธ Caveat**,
**๐๏ธ Design**, and **๐งช Verified**.Summary / Verification PR body, an
adaptive prose body from a git helper skill, plain ## Outcome sections, or
an unrelated generated badge footer unless the caller or repo template
explicitly asks for it.gh pr view --json body output or a concise source-backed summary
of that output.Final handoff / sync:
_target and injected placeholder props.pnpm check passed.Timeline:
Reboot status:
| Question | Answer |
|---|---|
| Where am I? | Closeout |
| Where am I going? | Final response after plan check and amend push |
| What is the goal? | Fix #4951 block placeholder list-item bug; done when repro fails before fix, passes after fix, checks/review pass, and PR/sync-back complete. |
| What have I learned? | #4951 is valid; the best boundary is the plugin pristine-state gate, not list-specific caller logic. |
| What have I done? | Reproduced, fixed, documented, verified, reviewed, opened PR #5029, and synced issue #4951. |
Open risks:
pnpm check passed before PR creation.