🤌 Rich text editor with real-time collaboration (Tiptap)

@rico.trevisan
thanks again for your work!
just something that I am not sure whether it is a bug or not. when I upload an image and then resize it to make it smaller, when I refresh the page the new smaller size is not saved but the old size is still in place. not sure whether this is something related to how tiptap works or the plugin itself. thanks

Thanks.

I’ll take a look!

v4.6.1

Bug Fix — Image resize not persisting

Fixed a bug where resizing an image would not persist after page refresh. The resized image would revert to its original size when the page was reloaded.

Root cause: The tiptap-extension-resizable extension was directly mutating the ProseMirror node’s attrs.width property instead of dispatching a proper transaction. ProseMirror uses immutable data structures, so direct mutations are not recorded in the document state and editor.getHTML() would not include the resized width.

Fix: The extension now uses editor.chain().updateAttributes('image', { width }) on mouseup to properly persist the width change via a ProseMirror transaction.

@rico.trevisan a small bug with collaboration that I can reproduce this way: when the editor is loaded and you start the editing by selecting a text (rather than just puttin the cursor somewhere and starrt writing) the name of the collaborator shows as “User: 2342345” to other collaborators rather than the value you set in the “user_name” field.

EDIT: this happens occasionally even in other cases

thanks and appreciate the work with this plugin

@rico.trevisan any chance you could check this in the future? thanks in advance

I’ll take a look this weekend. Thanks for the report.

Hey @rico.trevisan — first off, thanks for an excellent plugin, we’re using it heavily in production.

I think I’ve tracked down a reproducible bug where characters get silently dropped while typing when auto-binding is enabled (collab off) and delay set to 0. Wanted to share the diagnosis in case it’s useful.

Symptom: while typing in a focused editor, a chunk of recently-typed characters occasionally disappears. It’s racey and intermittent, but easy to trigger by typing quickly for a couple of seconds.

Root cause: a feedback loop between onUpdate and the autobind branch of update(). Walking through it:

  1. User types → onUpdate fires → instance.data.updateContent(contentHTML) writes to the bound field and sets isDebouncingDone = false.

  2. Debounce completes, isDebouncingDone = true, Bubble re-runs update() with the new properties.autobinding.

  3. But by the time update() runs, the user has already typed more keystrokes, so properties.autobinding is stale relative to editor.getHTML().

  4. The guard properties.autobinding !== editor.getHTML() is therefore true, and this branch runs:

editor.commands.setContent(properties.autobinding, false);
editor.commands.setTextSelection({ from: newFrom, to: newTo });

…which clobbers the in-flight keystrokes with the older saved HTML. Confirmed via a view.updateState trace: one transaction goes from 137 → 107 chars, stack traces straight to setContent from the plugin’s update function.

Suggested fix: ignore echoes of our own writes, and don’t yank content while the editor is focused. Roughly:

// in onUpdate, remember what we just published
instance.data._lastPublishedHTML = contentHTML;

// in update(), tighten the autobind branch
if (
  instance.data.editor_is_ready &&
  properties.bubble.auto_binding() &&
  !properties.collab_active &&
  instance.data.isDebouncingDone &&
  properties.autobinding !== editor.getHTML() &&
  properties.autobinding !== instance.data._lastPublishedHTML && // ignore self-echo
  !editor.isFocused                                              // don't overwrite live typing
) {
  editor.commands.setContent(properties.autobinding, false);
  ...
}

Happy to share a fuller repro or test a patched build if helpful. Thanks again for the great work!

@rico.trevisan Another one:
Closely related: when switching between pages quickly (single-page-app navigation, each page rendering a Tiptap editor bound to a different record), the content of the previous page sometimes overwrites the new page’s content. The new editor briefly mounts with the correct initialContent, then a delayed update() from the previous page’s autobind round-trip lands and calls setContent(staleHTML) on the wrong record. The result is page B showing page A’s text — and worse, if autobind then fires, page B’s record gets silently corrupted with page A’s content.

Previously I worked around this with a “Reset Tiptap” action that I’d call on page change, which forced a clean teardown. That action no longer appears to exist in the current version, so the workaround is gone. Could it be reinstated, or alternatively could update() short-circuit when the element is being torn down / when initialContent’s underlying record id has changed since the debounce started?

Sorry guys, I’ve not had the time to stop and work on this. I need to make the repo easier for you guys to clone and / or send PRs.

Actually, could you point your AI to the repo: GitHub - RicoTrevisan/tiptap-plugin-for-bubble: Repo created with Pled which includes the plugin code + libraries. · GitHub and ask it to make a PR?

I finally got around to it. Give it a test and see if it works.

Could you check if v4.6.2 fixes it?

@rico.trevisan, thanks for creating this plugin, it looks great!
We’d like to use it but realised @tiptap/ai-toolkit support isn’t part of the extensions list. Could you add support for the AI Toolkit?
I raised a feature request on GitHub and starred the repo :slight_smile:

Thanks, I’ll pick it up now.

v4.7.0 now has a toggle for AI Toolkit. Can you test to see if it works for you?

Thanks Rico, that was fast :rocket:! We’ll test it and report back.

Hi @rico.trevisan, I tested AI Toolkit with v4.7.1 against a Tiptap Cloud document - it works, great work!

Read the doc via REST API before and after typing: all 19 _hash values byte-identical, including on the block I edited in the editor. Also ran an execute-tool call server-side and it applied cleanly.

One follow-up if you’re open to it: could the element expose the AI editor context as a state?

Why it’s needed: every AI Toolkit REST call (fetch-tools, execute-tool,
stream-tool) takes editorContext as a required field. It’s derived purely from the
extension list, so it’s the same value on every call and only changes when the
extension list does.

I was able to get it via a workaround: import getEditorContext from esm.sh and
call it on the live editor, which turns out to be reachable at
document.querySelector(‘.ProseMirror’).editor:

const m = await import(‘https://esm.sh/@tiptap/ai-toolkit@0.3.0?deps=…’)
const ctx = m.getEditorContext(document.querySelector(‘.ProseMirror’).editor)

I think it would be great to have it exposed as a state, e.g. published when the editor is ready. It would also always match the element’s actual extension config, which a hand-maintained copy can’t guarantee across upgrades.

@branimir.ivanovv4.7.2 is up with the AI editor context as a state. What changed:

  • New AI editor context (JSON) state on the element. When the AI Toolkit toggle is on, it contains the editorContext your fetch-tools / execute-tool / stream-tool calls need — generated from the editor’s real extension config, so it can’t drift from the schema.

How to use it:

  1. Turn on AI Toolkit on the element.
  2. Read the AI editor context (JSON) state in a workflow and send it to your backend as the editorContext field.
  3. Keep signing JWTs server-side as before — this release only exposes the context; it doesn’t call Tiptap’s API or add an AI-provider integration.

Also fixed: toggling AI Toolkit now preserves your unsaved document instead of resetting it, and the state clears cleanly when disabled. One note: I exposed and tested the context end-to-end, but I don’t have a Tiptap subscription, so I couldn’t run the paid fetch-tools call myself. If you try it against your project, let me know it lines up.

v4.8.0

Line height & background color

Added line height and text background color, filling the gap next to the existing text color and font controls.

  • New Line Height toggle (off by default) — set or unset line height on a selection. Set line height / Unset line height actions.
  • New Background Color toggle (off by default) — set or unset the text background color, separate from text color and the highlight mark. Set background color / Unset background color actions.
  • Two new states: line_height and background_color.