diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md
index 9ce527bf..cf81d6d9 100644
--- a/docs/ABSTRACTIONS.md
+++ b/docs/ABSTRACTIONS.md
@@ -573,7 +573,7 @@ Defined in `src/components/tolariaEditorFormatting.tsx` and `src/components/tola
- `useImageLightbox` listens for `dblclick` on the rich-editor container and opens `ImageLightbox` only when the event target resolves to a viewable BlockNote image. The target resolver handles media wrappers, ignores image captions/resize controls, missing sources, and tiny tracking-style images, preserving BlockNote's ordinary single-click image selection path.
- The `/` slash menu remains the supported path for markdown-safe block transformations such as headings, quotes, list blocks, Mermaid diagrams, and whiteboards. Tolaria filters out BlockNote's toggle-heading and toggle-list variants because those do not map cleanly to the markdown note model.
- The block-handle side menu keeps only actions that survive Tolaria's markdown round-trip. Delete and table-header toggles remain available; BlockNote's `Colors` submenu is removed because block colors are not part of Tolaria's supported markdown surface. Tolaria renders the add-block button outside the drag handle so the handle stays next to the block content. The side menu aligns itself to the first rendered text line for the hovered block, so H1/H2 typography, line-height, wrapping, and theme changes do not need per-heading offsets. Block reordering uses a Tolaria-owned pointer gesture and direct BlockNote block moves instead of HTML5 `DataTransfer`, keeping it independent from Tauri's native file-drop system. Block-handle actions re-resolve the current live BlockNote block before mutating or dragging, so note reloads and sync churn cannot leave controls acting on stale block references.
-- BlockNote's table row/column handles are patched so stale or missing hovered-table state cancels the drag and hides handles instead of throwing. Add/remove row and column actions also validate the table position and cell indexes before resolving a ProseMirror `CellSelection`, so reloads or menu lag cannot turn stale handles into invalid table-selection positions. Browser and native table regressions should exercise row and column dragging plus add-menu actions because the state is tracked per orientation.
+- BlockNote's table row/column handles are patched so stale or missing hovered-table state cancels the drag and hides handles instead of throwing. Add/remove row and column actions also validate the table position and cell indexes before resolving a ProseMirror `CellSelection`, so reloads or menu lag cannot turn stale handles into invalid table-selection positions. Checklist checkbox handlers also re-resolve the live block before updating `checked`, making delayed clicks after note reloads a no-op instead of a stale block mutation. Browser and native table regressions should exercise row and column dragging plus add-menu actions because the state is tracked per orientation.
- `useNoteWikilinkDrop()` is the shared editor-drop abstraction for dragging note rows into either editor mode. It reads the existing note-retargeting drag payload, resolves the vault-relative stem, and inserts a canonical `[[wikilink]]` without hijacking unrelated plain-text drags.
- `plainTextPaste.ts` is the shared plain-text paste target registry. Rich BlockNote and raw CodeMirror surfaces register focused insertion targets, while ordinary focused text controls use DOM selection replacement, so the `Cmd+Shift+V` command can preserve caret/selection behavior without each surface inventing its own clipboard reader.
- `useTauriDragDropEvent()` owns the shared Tauri window drag/drop subscription and duplicate-unlisten cleanup used by native drop features.
diff --git a/patches/@blocknote__core@0.46.2.patch b/patches/@blocknote__core@0.46.2.patch
index 5aa4fce0..ec8de287 100644
--- a/patches/@blocknote__core@0.46.2.patch
+++ b/patches/@blocknote__core@0.46.2.patch
@@ -346,6 +346,20 @@ index b2761001278486a8b2ac1d10c82420b4994e96d9..c44e9c09100c6d1803bd71b2a338c43e
n.updateBlock(t.id, { props: { language: d } });
};
i.addEventListener("change", l), s = () => i.removeEventListener("change", l);
+@@ -1802,9 +1804,12 @@ const ln = () => ({
+ parseContent: ({ el: e, schema: t }) => Y(e, t, "checkListItem"),
+ render(e, t) {
+ const n = document.createDocumentFragment(), o = document.createElement("input");
+ o.type = "checkbox", o.checked = e.props.checked, e.props.checked && o.setAttribute("checked", ""), o.addEventListener("change", () => {
+- t.updateBlock(e, { props: { checked: !e.props.checked } });
++ const r = t.getBlock(e.id);
++ if (!r)
++ return;
++ t.updateBlock(r, { props: { checked: !r.props.checked } });
+ });
+ const r = document.createElement("p"), a = document.createElement("div");
+ return a.contentEditable = "false", a.appendChild(o), n.appendChild(a), n.appendChild(r), {
+ dom: n,
@@ -2621,6 +2623,9 @@ class On {
this.state.referencePos = o.getBoundingClientRect().toJSON(), this.emitUpdate(this.pluginState.triggerCharacter);
}
@@ -441,6 +455,24 @@ index dbb7fc33a9add7a96488349876bc56ad60111a3f..58c3cf181f25467d85cd8c3788b5b73d
editor.updateBlock(block.id, { props: { language } });
};
select.addEventListener("change", handleLanguageChange);
+diff --git a/src/blocks/ListItem/CheckListItem/block.ts b/src/blocks/ListItem/CheckListItem/block.ts
+index 682cbba711bd9ca107fe3940f44ab39fc757cfe0..2d2f871ac13836b736543b29a620580a7a3f1057 100644
+--- a/src/blocks/ListItem/CheckListItem/block.ts
++++ b/src/blocks/ListItem/CheckListItem/block.ts
+@@ -82,7 +82,12 @@ export const createCheckListItemBlockSpec = createBlockSpec(
+ checkbox.setAttribute("checked", "");
+ }
+ checkbox.addEventListener("change", () => {
+- editor.updateBlock(block, { props: { checked: !block.props.checked } });
++ const liveBlock = editor.getBlock(block.id);
++ if (!liveBlock) {
++ return;
++ }
++
++ editor.updateBlock(liveBlock, { props: { checked: !liveBlock.props.checked } });
+ });
+ // We use a
tag, because for
tags we'd need a element to put
+ // them in to be semantically correct, which we can't have due to the
diff --git a/src/editor/managers/ExtensionManager/extensions.ts b/src/editor/managers/ExtensionManager/extensions.ts
index 45f0acf8e5d869eb13a3c3d70e6f67059f86c598..2a4d15c8e8465181cc009ed9912cd284e0930251 100644
--- a/src/editor/managers/ExtensionManager/extensions.ts
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ae68385a..c4aa3418 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -16,7 +16,7 @@ overrides:
patchedDependencies:
'@blocknote/core@0.46.2':
- hash: a72012129a27526bdd0ce50ab708872257576135b87550211baacbd0ff8e7daf
+ hash: 93736b5b6fa4b9d257d510af50da44c5192f3e3115dfb9548a11f1a4865027e4
path: patches/@blocknote__core@0.46.2.patch
'@blocknote/react@0.46.2':
hash: e09f7011df33f4ff92c0d3fd8c9060e62f08997d076f3e438e1e82f8c1ab2f76
@@ -37,10 +37,10 @@ importers:
version: 0.78.0(zod@4.3.6)
'@blocknote/code-block':
specifier: ^0.46.2
- version: 0.46.2(@blocknote/core@0.46.2(patch_hash=a72012129a27526bdd0ce50ab708872257576135b87550211baacbd0ff8e7daf)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0))
+ version: 0.46.2(@blocknote/core@0.46.2(patch_hash=93736b5b6fa4b9d257d510af50da44c5192f3e3115dfb9548a11f1a4865027e4)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0))
'@blocknote/core':
specifier: ^0.46.2
- version: 0.46.2(patch_hash=a72012129a27526bdd0ce50ab708872257576135b87550211baacbd0ff8e7daf)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)
+ version: 0.46.2(patch_hash=93736b5b6fa4b9d257d510af50da44c5192f3e3115dfb9548a11f1a4865027e4)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)
'@blocknote/mantine':
specifier: ^0.46.2
version: 0.46.2(@floating-ui/dom@1.7.5)(@mantine/core@8.3.14(@mantine/hooks@8.3.14(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@8.3.14(react@19.2.4))(@mantine/utils@6.0.22(react@19.2.4))(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -5608,9 +5608,9 @@ snapshots:
'@bcoe/v8-coverage@1.0.2': {}
- '@blocknote/code-block@0.46.2(@blocknote/core@0.46.2(patch_hash=a72012129a27526bdd0ce50ab708872257576135b87550211baacbd0ff8e7daf)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0))':
+ '@blocknote/code-block@0.46.2(@blocknote/core@0.46.2(patch_hash=93736b5b6fa4b9d257d510af50da44c5192f3e3115dfb9548a11f1a4865027e4)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0))':
dependencies:
- '@blocknote/core': 0.46.2(patch_hash=a72012129a27526bdd0ce50ab708872257576135b87550211baacbd0ff8e7daf)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)
+ '@blocknote/core': 0.46.2(patch_hash=93736b5b6fa4b9d257d510af50da44c5192f3e3115dfb9548a11f1a4865027e4)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)
'@shikijs/core': 3.23.0
'@shikijs/engine-javascript': 3.23.0
'@shikijs/langs': 3.23.0
@@ -5618,7 +5618,7 @@ snapshots:
'@shikijs/themes': 3.23.0
'@shikijs/types': 3.22.0
- '@blocknote/core@0.46.2(patch_hash=a72012129a27526bdd0ce50ab708872257576135b87550211baacbd0ff8e7daf)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)':
+ '@blocknote/core@0.46.2(patch_hash=93736b5b6fa4b9d257d510af50da44c5192f3e3115dfb9548a11f1a4865027e4)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)':
dependencies:
'@emoji-mart/data': 1.2.1
'@handlewithcare/prosemirror-inputrules': 0.1.4(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6)
@@ -5670,7 +5670,7 @@ snapshots:
'@blocknote/mantine@0.46.2(@floating-ui/dom@1.7.5)(@mantine/core@8.3.14(@mantine/hooks@8.3.14(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@8.3.14(react@19.2.4))(@mantine/utils@6.0.22(react@19.2.4))(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
- '@blocknote/core': 0.46.2(patch_hash=a72012129a27526bdd0ce50ab708872257576135b87550211baacbd0ff8e7daf)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)
+ '@blocknote/core': 0.46.2(patch_hash=93736b5b6fa4b9d257d510af50da44c5192f3e3115dfb9548a11f1a4865027e4)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)
'@blocknote/react': 0.46.2(patch_hash=e09f7011df33f4ff92c0d3fd8c9060e62f08997d076f3e438e1e82f8c1ab2f76)(@floating-ui/dom@1.7.5)(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@mantine/core': 8.3.14(@mantine/hooks@8.3.14(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@mantine/hooks': 8.3.14(react@19.2.4)
@@ -5692,7 +5692,7 @@ snapshots:
'@blocknote/react@0.46.2(patch_hash=e09f7011df33f4ff92c0d3fd8c9060e62f08997d076f3e438e1e82f8c1ab2f76)(@floating-ui/dom@1.7.5)(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies:
- '@blocknote/core': 0.46.2(patch_hash=a72012129a27526bdd0ce50ab708872257576135b87550211baacbd0ff8e7daf)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)
+ '@blocknote/core': 0.46.2(patch_hash=93736b5b6fa4b9d257d510af50da44c5192f3e3115dfb9548a11f1a4865027e4)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)
'@emoji-mart/data': 1.2.1
'@floating-ui/react': 0.27.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@floating-ui/utils': 0.2.10
diff --git a/src/lib/blockNoteChecklist.regression.test.ts b/src/lib/blockNoteChecklist.regression.test.ts
new file mode 100644
index 00000000..79bbcb31
--- /dev/null
+++ b/src/lib/blockNoteChecklist.regression.test.ts
@@ -0,0 +1,82 @@
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { createCheckListItemBlockSpec } from '../../node_modules/@blocknote/core/src/blocks/ListItem/CheckListItem/block'
+
+const checkListItemSpec = createCheckListItemBlockSpec()
+
+type CheckListItemBlock = Parameters[0]
+type CheckListItemEditor = Parameters[1]
+type RenderedCheckListItem = ReturnType
+
+type CheckListItemControlEditor = {
+ getBlock: (id: string) => CheckListItemBlock | undefined
+ updateBlock: (block: CheckListItemBlock, update: { props: { checked: boolean } }) => void
+}
+
+function createCheckListItem(checked = false): CheckListItemBlock {
+ return {
+ id: 'check-list-item-1',
+ type: 'checkListItem',
+ props: { checked },
+ content: [],
+ children: [],
+ } as CheckListItemBlock
+}
+
+function renderCheckListItem(editor: CheckListItemControlEditor, checked = false) {
+ const block = createCheckListItem(checked)
+ const view = checkListItemSpec.implementation.render(
+ block,
+ editor as CheckListItemEditor,
+ ) as RenderedCheckListItem
+ const host = document.createElement('div')
+ host.appendChild(view.dom)
+ document.body.appendChild(host)
+
+ const checkbox = host.querySelector('input[type="checkbox"]')
+ if (!(checkbox instanceof HTMLInputElement)) throw new Error('Expected checklist checkbox')
+
+ return { block, checkbox, host, view }
+}
+
+function dispatchChange(checkbox: HTMLInputElement) {
+ checkbox.dispatchEvent(new window.Event('change'))
+}
+
+afterEach(() => {
+ document.body.replaceChildren()
+})
+
+describe('patched BlockNote checklist controls', () => {
+ it('ignores stale checkbox changes when the target checklist block disappeared', () => {
+ const editor: CheckListItemControlEditor = {
+ getBlock: vi.fn(() => undefined),
+ updateBlock: vi.fn(),
+ }
+
+ const { block, checkbox, view } = renderCheckListItem(editor)
+ checkbox.checked = true
+ dispatchChange(checkbox)
+
+ expect(editor.getBlock).toHaveBeenCalledWith(block.id)
+ expect(editor.updateBlock).not.toHaveBeenCalled()
+ view.destroy?.()
+ })
+
+ it('applies live checkbox changes to the current checklist block', () => {
+ const existingBlock = createCheckListItem()
+ const editor: CheckListItemControlEditor = {
+ getBlock: vi.fn(() => existingBlock),
+ updateBlock: vi.fn(),
+ }
+
+ const { block, checkbox, view } = renderCheckListItem(editor)
+ checkbox.checked = true
+ dispatchChange(checkbox)
+
+ expect(editor.getBlock).toHaveBeenCalledWith(block.id)
+ expect(editor.updateBlock).toHaveBeenCalledWith(existingBlock, {
+ props: { checked: true },
+ })
+ view.destroy?.()
+ })
+})
diff --git a/tests/smoke/rich-editor-reload-typing.spec.ts b/tests/smoke/rich-editor-reload-typing.spec.ts
index 423f2440..6d5236cb 100644
--- a/tests/smoke/rich-editor-reload-typing.spec.ts
+++ b/tests/smoke/rich-editor-reload-typing.spec.ts
@@ -13,6 +13,7 @@ let tempVaultDir: string
function isEditorTypingCrash(message: string): boolean {
return (
message.includes('beforeinput') ||
+ message.includes('Block with ID') ||
message.includes('stale editor view') ||
message.includes('Cannot read properties') ||
message.includes('undefined is not an object') ||
@@ -72,6 +73,44 @@ async function expectNoteFileToContain(filePath: string, marker: string): Promis
await expect.poll(() => fs.readFileSync(filePath, 'utf8'), { timeout: 10_000 }).toContain(marker)
}
+function writeChecklistNote(filePath: string, marker: string, checked = false): void {
+ fs.writeFileSync(filePath, `---
+Is A: Note
+Status: Active
+---
+
+# Note B
+
+- [${checked ? 'x' : ' '}] Toggle me
+- [ ] Keep me
+
+${marker}
+`, 'utf8')
+}
+
+function checklistCheckbox(page: Page, index: number) {
+ return page.locator('.bn-block-content[data-content-type="checkListItem"] input[type="checkbox"]').nth(index)
+}
+
+async function retainCurrentChecklistCheckbox(page: Page): Promise {
+ await page.evaluate(() => {
+ const testWindow = window as typeof window & { __staleChecklistCheckbox?: HTMLInputElement | null }
+ testWindow.__staleChecklistCheckbox = document.querySelector(
+ '.bn-block-content[data-content-type="checkListItem"] input[type="checkbox"]',
+ )
+ })
+}
+
+async function dispatchRetainedChecklistChange(page: Page): Promise {
+ await page.evaluate(() => {
+ const testWindow = window as typeof window & { __staleChecklistCheckbox?: HTMLInputElement | null }
+ const checkbox = testWindow.__staleChecklistCheckbox
+ if (!checkbox) throw new Error('Expected retained checklist checkbox')
+ checkbox.checked = !checkbox.checked
+ checkbox.dispatchEvent(new Event('change', { bubbles: true }))
+ })
+}
+
async function reloadVault(page: Page): Promise {
await triggerMenuCommand(page, 'vault-reload')
await expect(page.getByText(/Vault reloaded \(\d+ entries\)/).last()).toBeVisible({
@@ -118,3 +157,30 @@ test('@smoke typing after a rich-editor reload and note switch stays usable', as
await page.waitForTimeout(500)
expect(crashes).toEqual([])
})
+
+test('checklist toggles after a rich-editor reload ignore stale checkbox events', async ({ page }) => {
+ const crashes = trackEditorTypingCrashes(page)
+ const noteBPath = path.join(tempVaultDir, 'note', 'note-b.md')
+ const initialMarker = `initial checklist body ${Date.now()}`
+ const reloadMarker = `reloaded checklist body ${Date.now()}`
+
+ writeChecklistNote(noteBPath, initialMarker)
+ await openNote(page, 'Note B')
+ await expect(checklistCheckbox(page, 0)).not.toBeChecked()
+ await retainCurrentChecklistCheckbox(page)
+
+ writeChecklistNote(noteBPath, reloadMarker)
+ await reloadVault(page)
+ await openNote(page, 'Alpha Project')
+ await openNote(page, 'Note B')
+ await expect(page.locator('.bn-editor')).toContainText(reloadMarker)
+
+ await dispatchRetainedChecklistChange(page)
+
+ const liveCheckbox = checklistCheckbox(page, 0)
+ await liveCheckbox.click()
+ await expect(liveCheckbox).toBeChecked()
+ await expectNoteFileToContain(noteBPath, '- [x] Toggle me')
+ await page.waitForTimeout(500)
+ expect(crashes).toEqual([])
+})