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
{{ message }}
Repository navigation
Undo a Slash pick with one Mod-z, and hide rows that lift their own block #332
After /h2 and Enter, one Mod-z gives different results by typing speed, because a Slash pick has no undo boundary of its own. It can also remove the typing before the /, and in a shared pad that lands on shared text. Two rows also undo their own block: "Bullet List" in an empty bullet item, and "Blockquote" inside a blockquote, each lift the line out.
After the fix, a pick is exactly one undo step, and each row does what its label says.
Acceptance criteria
Type /h2 and press Enter. The pick adds exactly one step to the undo stack, at any typing speed.
Text typed right after a pick is its own undo step. It does not merge into the pick.
On desktop, one Mod-z after the pick gives back the /h2 paragraph, and the Slash menu opens on it again.
In an empty item, the row whose type equals the nearest list is absent. Example: /bul in a bullet item shows no "Bullet List" row.
The other list rows stay. "Ordered List" inside a bullet list still shows, as the toolbar button does.
Inside any blockquote, including a list item inside a quote, the "Blockquote" row is absent.
Two new Cypress cases pass. Each case fails when its fix is removed (prove it by sabotage).
Blocked by
None — can start now.
Agent brief
Type: AFK — an agent can finish this alone.
Category: bug
Current behavior:
The pick runs deleteRange(range), then item.run(editor), and sets no undo boundary (apps/webapp/src/components/TipTap/extensions/slash-menu/slash-menu.ts:63-70).
The Yjs UndoManager merges changes less than 500 ms apart (captureTimeout = 500, yjs 13.6.32 dist/yjs.mjs:3580). The pad's yUndoPlugin sets no other value (@tiptap/y-tiptapdist/y-tiptap.js:2866-2869).
So a fast /h2 Enter merges the pick into the typing. A slow one does not. This is code-traced. No browser run has shown it yet; the new test does that first.
The Bullet List, Ordered List, Task List and Blockquote rows use can: always (apps/webapp/src/components/TipTap/slash/slashItems.ts:55-86).
toggleList with the same list type calls liftListItem (@tiptap/coresrc/commands/toggleList.ts). toggleWrap calls lift when a blockquote is already active (@tiptap/coresrc/commands/toggleWrap.ts:25-28).
Desired behavior:
Stop Yjs capture before deleteRange and again after item.run. The first call keeps the pick apart from the typing. The second keeps the next keys apart from the pick.
The heading toc-id stamp runs in appendTransaction (@tiptap/extension-unique-id 3.31.3 dist/index.js:118). That lands in the same dispatch, so the second call does not split it from the heading.
After a pick, @tiptap/suggestion 3.31.3 sets no dismissed range. It sets one only on exit. So the restored /h2 opens the list again. The test pins that.
Hide a list row when the nearest list ancestor has the same type. Hide Blockquote when editor.isActive('blockquote').
Where to start: Line numbers are hints as of 2026-09-28; the agent searches by symbol.
Add apps/webapp/src/components/TipTap/slash/pickUndoBoundary.ts. It exports one function that calls yUndoPluginKey.getState(editor.state)?.undoManager.stopCapturing(). The same handle is used at toolbar/desktop/applyImportedContent.ts:20. The webapp and @tiptap/extension-collaboration resolve the same @tiptap/y-tiptap copy (3.0.9), so this key finds the pad's undo plugin. Optional chaining covers the UndoRedo path, where there is no Yjs undo.
In slash-menu.tscommand, call the helper before deleteRange and after item.run.
Add apps/webapp/src/components/TipTap/slash/slashContext.ts. It is the one home for caret checks that row can functions use. Put nearestListType(editor) there. Build it with findParentNode from @tiptap/core over bulletList, orderedList and taskList.
Do not use isActive for lists. Lists nest, and isActive is true for any list ancestor.
In slashItems.ts, replace can: always on the three list rows and on Blockquote.
Rules that apply:
CONTEXT.md §Pad tools: each Slash row runs an existing editor command.
apps/webapp/src/components/TipTap/CLAUDE.md §Editor Performance: items() runs on every keystroke, so keep the new checks cheap. Walk only the caret depth.
AGENTS.md §Test Policy: the undo case is allowed under (c), an ordering bug that hand checks cannot pin. The rows case is allowed under (b): the defect is on main since commit e7271cc54. Prove each by sabotage.
AGENTS.md §Test Orchestration And Authoring: use realType. cy.realPress is a parent command, so gate on editor focus first.
Verify:
Add the undo case to apps/webapp/cypress/e2e/editor/slash/slash-menu.cy.ts:
The spec's beforeEach visits /editor with persist: true. That loads Collaboration on a local Y.Doc, so the pad has a Yjs undo manager. Without it the playground uses plain UndoRedo (apps/webapp/src/components/TipTap/TipTap.tsx:249). Keep persist: true.
After startIn, stop capture once, because its setContent would merge with fast typing.
Reach the undo manager through window._editor.state.plugins. Find the plugin whose key starts with y-undo$, then call getState(state).undoManager. new PluginKey('y-undo') (@tiptap/y-tiptapdist/y-tiptap.js:35) gives the key y-undo$, or y-undo$1 if a second copy loads. Fail the test when the lookup finds nothing. Add no window globals.
realType('/h2'). Read undoStack.length as L. Press Enter, then realType('x') at once. Assert the length is L + 2.
Pick the modifier like cypress/e2e/editor/find/caret-find.cy.ts:18: Meta on darwin, Control elsewhere. keyboard-shortcuts.cy.js presses Mod-z but asserts nothing, so do not copy it.
Mod-z once: the block is Heading 2 with empty text. Mod-z again: the block is a paragraph /h2, and [data-testid="slash-menu"] exists.
Do not add a timed wait.
Add the rows case to the same spec:
Start in <h1>Doc</h1><ul><li><p></p></li></ul> and type /. Assert no [data-slash-item="bulletList"], and assert orderedList and taskList are present.
Start in <h1>Doc</h1><blockquote><p></p></blockquote> and type /. Assert no [data-slash-item="blockquote"].
From the repo root, run bun run check.
Start the local stack with make dev-local from the repo root. On first run it writes .env.local, which bun run dev in apps/webapp needs. Note the webapp port; it is not always 3000 (AGENTS.md §Test Policy; README.md local development).
From apps/webapp, run bunx cypress run --spec cypress/e2e/editor/slash/slash-menu.cy.ts --env EDITOR_BASE_URL=http://localhost:<port>. All cases pass.
Sabotage, one at a time, and watch a case fail each time:
Remove the helper call before deleteRange.
Remove the helper call after item.run.
Restore can: always on Bullet List.
Restore can: always on Blockquote.
Put each fix back after it fails. Run the sabotage locally: a slow runner can stretch the gap before Enter past 500 ms and hide the merge.
No look changes, so no light and dark check is needed. The phone sheet uses the same filterSlashItems, so it gets the same rows.
Out of scope
Code Block in a list item lifts the item before it converts (@tiptap/coresrc/commands/setNode.ts falls back to clearNodes). The toolbar does the same. Accept it.
"Ordered List" in a bullet list changes the whole list. That matches the toolbar.
A second can check at pick time. On failure it would close the list and leave a dead /query.
Parent
#328. Related: #251
What to build
After
/h2and Enter, one Mod-z gives different results by typing speed, because a Slash pick has no undo boundary of its own. It can also remove the typing before the/, and in a shared pad that lands on shared text. Two rows also undo their own block: "Bullet List" in an empty bullet item, and "Blockquote" inside a blockquote, each lift the line out.After the fix, a pick is exactly one undo step, and each row does what its label says.
Acceptance criteria
/h2and press Enter. The pick adds exactly one step to the undo stack, at any typing speed./h2paragraph, and the Slash menu opens on it again./bulin a bullet item shows no "Bullet List" row.Blocked by
None — can start now.
Agent brief
Type: AFK — an agent can finish this alone.
Category: bug
Current behavior:
deleteRange(range), thenitem.run(editor), and sets no undo boundary (apps/webapp/src/components/TipTap/extensions/slash-menu/slash-menu.ts:63-70).UndoManagermerges changes less than 500 ms apart (captureTimeout = 500,yjs13.6.32dist/yjs.mjs:3580). The pad'syUndoPluginsets no other value (@tiptap/y-tiptapdist/y-tiptap.js:2866-2869)./h2Enter merges the pick into the typing. A slow one does not. This is code-traced. No browser run has shown it yet; the new test does that first.can: always(apps/webapp/src/components/TipTap/slash/slashItems.ts:55-86).toggleListwith the same list type callsliftListItem(@tiptap/coresrc/commands/toggleList.ts).toggleWrapcallsliftwhen a blockquote is already active (@tiptap/coresrc/commands/toggleWrap.ts:25-28).Desired behavior:
deleteRangeand again afteritem.run. The first call keeps the pick apart from the typing. The second keeps the next keys apart from the pick.toc-idstamp runs inappendTransaction(@tiptap/extension-unique-id3.31.3dist/index.js:118). That lands in the same dispatch, so the second call does not split it from the heading.@tiptap/suggestion3.31.3 sets no dismissed range. It sets one only on exit. So the restored/h2opens the list again. The test pins that.editor.isActive('blockquote').Where to start: Line numbers are hints as of 2026-09-28; the agent searches by symbol.
apps/webapp/src/components/TipTap/slash/pickUndoBoundary.ts. It exports one function that callsyUndoPluginKey.getState(editor.state)?.undoManager.stopCapturing(). The same handle is used attoolbar/desktop/applyImportedContent.ts:20. The webapp and@tiptap/extension-collaborationresolve the same@tiptap/y-tiptapcopy (3.0.9), so this key finds the pad's undo plugin. Optional chaining covers theUndoRedopath, where there is no Yjs undo.@list (Add a pad @ list where Members open a Comment and Headings insert a link #349) will use the same undo boundary.slash-menu.tscommand, call the helper beforedeleteRangeand afteritem.run.apps/webapp/src/components/TipTap/slash/slashContext.ts. It is the one home for caret checks that rowcanfunctions use. PutnearestListType(editor)there. Build it withfindParentNodefrom@tiptap/coreoverbulletList,orderedListandtaskList.isActivefor lists. Lists nest, andisActiveis true for any list ancestor.slashItems.ts, replacecan: alwayson the three list rows and on Blockquote.Rules that apply:
CONTEXT.md§Pad tools: each Slash row runs an existing editor command.apps/webapp/src/components/TipTap/CLAUDE.md§Editor Performance:items()runs on every keystroke, so keep the new checks cheap. Walk only the caret depth.AGENTS.md§Test Policy: the undo case is allowed under (c), an ordering bug that hand checks cannot pin. The rows case is allowed under (b): the defect is onmainsince commite7271cc54. Prove each by sabotage.AGENTS.md§Test Orchestration And Authoring: userealType.cy.realPressis a parent command, so gate on editor focus first.Verify:
apps/webapp/cypress/e2e/editor/slash/slash-menu.cy.ts:beforeEachvisits/editorwithpersist: true. That loadsCollaborationon a local Y.Doc, so the pad has a Yjs undo manager. Without it the playground uses plainUndoRedo(apps/webapp/src/components/TipTap/TipTap.tsx:249). Keeppersist: true.startIn, stop capture once, because itssetContentwould merge with fast typing.window._editor.state.plugins. Find the plugin whosekeystarts withy-undo$, then callgetState(state).undoManager.new PluginKey('y-undo')(@tiptap/y-tiptapdist/y-tiptap.js:35) gives the keyy-undo$, ory-undo$1if a second copy loads. Fail the test when the lookup finds nothing. Add no window globals.realType('/h2'). ReadundoStack.lengthas L. Press Enter, thenrealType('x')at once. Assert the length is L + 2.cypress/e2e/editor/find/caret-find.cy.ts:18:Metaondarwin,Controlelsewhere.keyboard-shortcuts.cy.jspresses Mod-z but asserts nothing, so do not copy it./h2, and[data-testid="slash-menu"]exists.<h1>Doc</h1><ul><li><p></p></li></ul>and type/. Assert no[data-slash-item="bulletList"], and assertorderedListandtaskListare present.<h1>Doc</h1><blockquote><p></p></blockquote>and type/. Assert no[data-slash-item="blockquote"].bun run check.make dev-localfrom the repo root. On first run it writes.env.local, whichbun run devinapps/webappneeds. Note the webapp port; it is not always 3000 (AGENTS.md§Test Policy;README.mdlocal development).apps/webapp, runbunx cypress run --spec cypress/e2e/editor/slash/slash-menu.cy.ts --env EDITOR_BASE_URL=http://localhost:<port>. All cases pass.deleteRange.item.run.can: alwayson Bullet List.can: alwayson Blockquote.Put each fix back after it fails. Run the sabotage locally: a slow runner can stretch the gap before Enter past 500 ms and hide the merge.
filterSlashItems, so it gets the same rows.Out of scope
@tiptap/coresrc/commands/setNode.tsfalls back toclearNodes). The toolbar does the same. Accept it.cancheck at pick time. On failure it would close the list and leave a dead/query.