[bug][ai-chat] Комментарии агента сбрасывают курсор в конец статьи (регрессия) #152
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Симптом
Когда агент (AI-чат / MCP) оставляет комментарий к статье, курсор в открытом редакторе прыгает в самый конец документа — на последний символ. Раньше такого не было.
Воспроизведение:
Причина
Привязка подсветки комментария делается в пакете
@docmost/mcp:createComment(packages/mcp/src/client.ts) вызываетmutatePageContent(...), которая открывает «живой» Yjs-документ (тот же, что открыт в браузере), накладывает markcommentчерезapplyAnchorInDocи пишет результат обратно.Запись обратно в
packages/mcp/src/lib/collaboration.tsсделана как полная замена документа — весь фрагмент удаляется и пересобирается заново:Yjs — это CRDT: у каждого куска текста свой идентификатор, и курсор в редакторе «приклеен» к этим идентификаторам через y-prosemirror. При полном удалении + пересборке все старые идентификаторы исчезают. Удалённое обновление прилетает в открытый редактор, y-prosemirror пытается восстановить позицию курсора по старому id — а его больше нет, позиция не резолвится, выделение схлопывается в дефолт (конец документа). Отсюда прыжок на последний символ.
Этот разрушительный write-back — общий путь записи для всех инструментов агента (правка текста, вставка/удаление узлов и т.д.), но именно на комментариях это особенно заметно: комментарий ничего не меняет в тексте, а курсор всё равно улетает.
Почему это регрессия
До коммита
4201f0a3(«feat(comments): make AI comments inline-only with robust anchoring») шаг привязки комментария молча проваливался — mark по факту не накладывался, реальной мутации живого документа не происходило, курсор не трогался. После него привязка стала реально срабатывать, и теперь каждый комментарий проходит через ту самую полную замену, которая дёргает курсор. Сам по себе фикс привязки корректен — проблема в разрушительном способе записи в документ.Предлагаемое решение
Заменить полную замену в
mutatePageContentнаupdateYFragmentизy-prosemirror— это ровно тот механизм, которым редактор синхронит свои правки в Yjs. Он делает структурный диф нового документа против текущего фрагмента и меняет только реально изменившиеся узлы, сохраняя Yjs-идентификаторы неизменных узлов. Курсор в открытом редакторе остаётся на месте.Эскиз (
packages/mcp/src/lib/collaboration.ts):Поведение
updateYFragmentпроверено по исходнику (y-prosemirror@1.3.7): при пустомmappingон всё равно дифит против заполненного фрагмента — ищет совпадающий префикс/суффикс детей, у текстовых узлов применяет дельта-формат (updateYText), у элементов рекурсивно сравнивает и трогает только изменённое. Для комментария это сводится к наложению markcommentна нужный диапазон черезformat(), без удаления остального.Затронутые файлы
packages/mcp/src/lib/collaboration.ts— заменить write-back вmutatePageContent; добавить импорты и memoized-schema (по образцуpackages/mcp/src/lib/diff.ts); сохранить диагностикуfindUnstorableAttrна ошибке кодирования.packages/mcp/package.json— добавитьy-prosemirrorв прямые зависимости (сейчас транзитивная).pnpm --filter @docmost/mcp build(сервер грузит@docmost/mcpизbuild/).Зона влияния / риски
mutatePageContent(правка текста, узлы, привязка комментариев,replacePageContent/update_page*). Это строгое улучшение: минимальный диф вместо полной замены — меньше шума в коллаборации, сохраняются block-id, не сбрасываются чужие курсоры.updateYFragment— это штатная функция привязки самого редактора (prosemirrorToYXmlFragmentиспользует её так же), так что использование поддерживаемое.Тестирование
packages/mcp/test/mock/create-comment.test.mjsи юнит-тесты пакета.commentреально наложен на диапазон.