You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
That spec expects one Backspace to delete the image. Its comment names joinBackward.
The hover half of that spec was fixed in 2.0.0. The image delete was left as the expected result. See the CHANGELOG note that a text caret keeps editing text. That is why this stayed unfixed.
The same library path is already filed for a Divider in #340. That issue is a different node.
ProseMirror's author says an app that wants another result should put a command in front of joinBackward. See prosemirror-commands#6.
What happened
This is planned for the next release of @docs.plus/extension-hypermultimedia.
Edward Saperia reported this on 2026-09-26. He wrote: "I want to remove this gap under the picture but if I press backspace the picture is deleted".
A heading sits above an image. An empty line sits under the image, and a bullet list follows. He presses Backspace to remove the empty line. The image is deleted instead.
The latest version on npm is 2.0.0, dated 2026-08-11. Version 2.1.0, dated 2026-09-17, is in the CHANGELOG but is not on npm yet. See #358. [Unreleased] in the package CHANGELOG has no Fixed entry yet. The fix belongs in the next release after 2.1.0.
What we expect
Backspace in the empty line under an image removes that line and keeps the image. The caret ends after the image.
The same holds for every media type the resize gripper accepts. Delete at the end of the image line does the same.
A second Backspace is the maintainer's choice. No Google Docs, Word, Notion, or Slack help page was found that states that press.
Steps to reproduce
Open a pad you can edit.
Add a heading, then an image, then an empty paragraph, then a bullet list.
Put the caret at the start of the empty paragraph.
Press Backspace once.
The empty paragraph should go, and the image should stay. The report says the image is deleted.
Also put the caret at the end of the paragraph that holds the image, and press Delete.
The chat report includes a screenshot. It is not attached here. The maintainer will drag it into this issue.
Findings
Read from code, not measured. This report does not press a key in an editor.
The pad sets Image to inline: true and allowBase64: true in TipTap.tsx. Video, audio, and the embed nodes stay block nodes. The clean-room page uses a block image unless the query is inlineImage=true.
Every media node is a leaf. In prosemirror-model 1.25.11, NodeType.isAtom is true for every leaf. So every media node is an atom. selectable is not false, so the node can be selected.
The base keymap binds Backspace to deleteSelection, then joinBackward, then selectNodeBackward. That chain is in prosemirror-commands 1.7.2, through @tiptap/pm 3.31.3.
joinBackward calls deleteBarrier first. If the current textblock is empty and the node before can be selected, it deletes the empty textblock and selects that node. If the node before is an atom at the parent depth, it deletes the atom.
The clean-room spec places a block image before a paragraph that has text. The empty-textblock branch does not run. The atom branch deletes the image. That file is the earlier record. It was not re-run here.
A block atom before an empty paragraph takes the empty-textblock branch first. The first Backspace selects the atom. A second Backspace is deleteSelection, which deletes it. Read from code, not measured.
A pad image is inline, so it lives inside a paragraph. A following empty paragraph meets another paragraph. deleteBarrier joins them, and the image stays. Read from code, not measured.
The image node view root is a div. The default display is block. A caret after the image can sit on the next visual line inside the same paragraph. joinBackward does not run, because the caret is not at the start of the textblock. The guide says the browser then handles Backspace. That key was not measured.
handleMediaDeleteKey in media-resize-controls.ts returns false for a focused TextSelection. Hover does not delete the image while the caret is in text. Read from code, not measured. The same spec already asserts that hover case.
Image stopEvent returns true only for events inside the caption. It does not handle this Backspace.
The extension binds no Backspace command of its own. The symbol that deletes a leaf atom is joinBackward, in prosemirror-commands src/commands.ts.
No settled rule in the root CLAUDE.md forbids this fix.
On 2026-09-29, dist/index.js was dated 2026-09-28 and the newest source file was dated 2026-09-22. dist was not stale, and it was not rebuilt.
Reference points
joinBackward joins or lifts when the caret is at the start of a textblock. selectNodeBackward selects the node before that textblock when joining is not allowed.
The ProseMirror guide says the base keymap chains deleteSelection, joinBackward, and selectNodeBackward. When none apply, the browser runs Backspace inside the textblock.
prosemirror-commands#6. Marijn Haverbeke writes that deleting an atom block on Backspace is intentional. He tells the app to put its own command in front of joinBackward.
Google Docs, Word, Notion, and Slack help pages do not state this key. Those claims are left out.
Acceptance criteria
Backspace at the start of an empty paragraph under an image removes that paragraph and keeps the image. The caret ends after the image.
The same holds for every node MediaResizeGripper accepts: image, video, audio, YouTube, Vimeo, SoundCloud, Spotify, and Loom.
The maintainer writes, in a comment here, what a second Backspace does. The change follows that comment. It does not invent the second press.
Delete at the end of the image line removes the following empty paragraph and keeps the image. The caret ends after the image.
Hover on an image does not delete it when the caret is in text elsewhere.
The media toolbar, the resize gripper, undo, markdown round trip, and HTML round trip still work.
A list and a heading next to an image still work.
A clean-room Cypress spec under extensions/extension-hypermultimedia/cypress/e2e/ fails before the fix. It asserts rendered DOM, not only node attrs. The old "image is deleted" assertion in delete-collision.cy.ts is updated in the same change. Its hover case stays.
CHANGELOG.md has a Fixed entry under [Unreleased]. Package docs note the key if readers need it. No release command is run in the fix.
Blocked by
None.
Agent brief
Type: HITL for the second Backspace. AFK for the first press, the Delete press, the spec, and the CHANGELOG entry.
Category: bug
Current behavior: Read from code, not measured. A leaf media node is an atom. joinBackward deletes that atom when it is the block before a textblock that is not empty. An empty textblock selects the atom instead. A pad image is inline, so a real empty paragraph after it should join and keep the image. A block-level node view can still put the caret on a visual line after the image, inside the same paragraph, where joinBackward does not run. handleMediaDeleteKey does not take a focused text caret.
Desired behavior: The first Backspace, and Delete at the end of the image line, remove the empty paragraph and keep the media node. The caret ends after the media node. The second Backspace follows the maintainer comment.
Where to start: Put a command in front of joinBackward and joinForward for these nodes. Search joinBackward in prosemirror-commands src/commands.ts, handleMediaDeleteKey in extensions/extension-hypermultimedia/src/utils/media-resize-controls.ts, inline() in src/nodes/image/image.ts, and the node view div in src/nodes/image/nodeView.ts. The pad option is in apps/webapp/src/components/TipTap/TipTap.tsx. The spec to update is cypress/e2e/delete/delete-collision.cy.ts. The playground image is inline only with inlineImage=true in test/playground/main.ts. File names are hints as of 2026-09-29.
Rules that apply:extensions/CLAUDE.md §Hypermultimedia Extension. Cypress checks for resize and loading assert rendered DOM, not node attrs alone. apps/webapp/src/components/TipTap/CLAUDE.md §Editor Performance: a leaf node view must not expose contentDOM. AGENTS.md §Test Policy allows this spec because it pins a reported bug. The release-extensions skill: add a CHANGELOG entry, and do not publish from the fix. After any extension source change, run bunx tsup in the package. No settled rule in the root CLAUDE.md forbids this fix.
Verify: Run only the new spec and delete-collision.cy.ts from extensions/extension-hypermultimedia. Do not run the whole webapp Jest suite. Then press Backspace on a real pad, with the caret in the empty line under an image, and confirm the image stays. Check one block node, such as YouTube, the same way.
Related
No GitHub issue records this bug. The earlier record is the clean-room spec
delete-collision.cy.ts, added in667d4acc1.That spec expects one Backspace to delete the image. Its comment names
joinBackward.The hover half of that spec was fixed in 2.0.0. The image delete was left as the expected result. See the CHANGELOG note that a text caret keeps editing text. That is why this stayed unfixed.
The same library path is already filed for a Divider in #340. That issue is a different node.
ProseMirror's author says an app that wants another result should put a command in front of
joinBackward. See prosemirror-commands#6.What happened
This is planned for the next release of
@docs.plus/extension-hypermultimedia.Edward Saperia reported this on 2026-09-26. He wrote: "I want to remove this gap under the picture but if I press backspace the picture is deleted".
A heading sits above an image. An empty line sits under the image, and a bullet list follows. He presses Backspace to remove the empty line. The image is deleted instead.
The latest version on npm is 2.0.0, dated 2026-08-11. Version 2.1.0, dated 2026-09-17, is in the CHANGELOG but is not on npm yet. See #358.
[Unreleased]in the package CHANGELOG has no Fixed entry yet. The fix belongs in the next release after 2.1.0.What we expect
Backspace in the empty line under an image removes that line and keeps the image. The caret ends after the image.
The same holds for every media type the resize gripper accepts. Delete at the end of the image line does the same.
A second Backspace is the maintainer's choice. No Google Docs, Word, Notion, or Slack help page was found that states that press.
Steps to reproduce
The empty paragraph should go, and the image should stay. The report says the image is deleted.
Also put the caret at the end of the paragraph that holds the image, and press Delete.
The chat report includes a screenshot. It is not attached here. The maintainer will drag it into this issue.
Findings
Read from code, not measured. This report does not press a key in an editor.
Imagetoinline: trueandallowBase64: trueinTipTap.tsx. Video, audio, and the embed nodes stay block nodes. The clean-room page uses a block image unless the query isinlineImage=true.NodeType.isAtomis true for every leaf. So every media node is an atom.selectableis not false, so the node can be selected.deleteSelection, thenjoinBackward, thenselectNodeBackward. That chain is in prosemirror-commands 1.7.2, through@tiptap/pm3.31.3.joinBackwardcallsdeleteBarrierfirst. If the current textblock is empty and the node before can be selected, it deletes the empty textblock and selects that node. If the node before is an atom at the parent depth, it deletes the atom.deleteSelection, which deletes it. Read from code, not measured.deleteBarrierjoins them, and the image stays. Read from code, not measured.div. The defaultdisplayisblock. A caret after the image can sit on the next visual line inside the same paragraph.joinBackwarddoes not run, because the caret is not at the start of the textblock. The guide says the browser then handles Backspace. That key was not measured.handleMediaDeleteKeyinmedia-resize-controls.tsreturns false for a focusedTextSelection. Hover does not delete the image while the caret is in text. Read from code, not measured. The same spec already asserts that hover case.stopEventreturns true only for events inside the caption. It does not handle this Backspace.joinBackward, in prosemirror-commandssrc/commands.ts.CLAUDE.mdforbids this fix.dist/index.jswas dated 2026-09-28 and the newest source file was dated 2026-09-22.distwas not stale, and it was not rebuilt.Reference points
joinBackwardjoins or lifts when the caret is at the start of a textblock.selectNodeBackwardselects the node before that textblock when joining is not allowed.deleteSelection,joinBackward, andselectNodeBackward. When none apply, the browser runs Backspace inside the textblock.joinBackward.Acceptance criteria
MediaResizeGripperaccepts: image, video, audio, YouTube, Vimeo, SoundCloud, Spotify, and Loom.extensions/extension-hypermultimedia/cypress/e2e/fails before the fix. It asserts rendered DOM, not only node attrs. The old "image is deleted" assertion indelete-collision.cy.tsis updated in the same change. Its hover case stays.CHANGELOG.mdhas a Fixed entry under[Unreleased]. Package docs note the key if readers need it. No release command is run in the fix.Blocked by
None.
Agent brief
Type: HITL for the second Backspace. AFK for the first press, the Delete press, the spec, and the CHANGELOG entry.
Category: bug
Current behavior: Read from code, not measured. A leaf media node is an atom.
joinBackwarddeletes that atom when it is the block before a textblock that is not empty. An empty textblock selects the atom instead. A pad image is inline, so a real empty paragraph after it should join and keep the image. A block-level node view can still put the caret on a visual line after the image, inside the same paragraph, wherejoinBackwarddoes not run.handleMediaDeleteKeydoes not take a focused text caret.Desired behavior: The first Backspace, and Delete at the end of the image line, remove the empty paragraph and keep the media node. The caret ends after the media node. The second Backspace follows the maintainer comment.
Where to start: Put a command in front of
joinBackwardandjoinForwardfor these nodes. SearchjoinBackwardin prosemirror-commandssrc/commands.ts,handleMediaDeleteKeyinextensions/extension-hypermultimedia/src/utils/media-resize-controls.ts,inline()insrc/nodes/image/image.ts, and the node viewdivinsrc/nodes/image/nodeView.ts. The pad option is inapps/webapp/src/components/TipTap/TipTap.tsx. The spec to update iscypress/e2e/delete/delete-collision.cy.ts. The playground image is inline only withinlineImage=trueintest/playground/main.ts. File names are hints as of 2026-09-29.Rules that apply:
extensions/CLAUDE.md§Hypermultimedia Extension. Cypress checks for resize and loading assert rendered DOM, not node attrs alone.apps/webapp/src/components/TipTap/CLAUDE.md§Editor Performance: a leaf node view must not exposecontentDOM.AGENTS.md§Test Policy allows this spec because it pins a reported bug. The release-extensions skill: add a CHANGELOG entry, and do not publish from the fix. After any extension source change, runbunx tsupin the package. No settled rule in the rootCLAUDE.mdforbids this fix.Verify: Run only the new spec and
delete-collision.cy.tsfromextensions/extension-hypermultimedia. Do not run the whole webapp Jest suite. Then press Backspace on a real pad, with the caret in the empty line under an image, and confirm the image stays. Check one block node, such as YouTube, the same way.