RS

Cursor-style inline diff с откатами: snapshot-based архитектура

Invalid Date мин чтения

В Skycode (мой форк VS Code с AI-агентом) была задача: показывать предложенные AI изменения как inline-diff в самом редакторе, с возможностью принять или отклонить каждый блок отдельно. И откатить весь набор изменений к любому из предыдущих сообщений в чате.

Постановка задачи

Cursor делает это красиво, но закрыто. В open-source аналогах (Continue, Cline) этой системы либо нет, либо она работает поверх чужих файлов через VS Code API workspace.applyEdit() — а это значит каждое изменение записывается в файл сразу, без возможности раскатать.

Я хотел:

  • Показ изменений inline без записи в файл
  • Accept/Reject по блокам
  • Полный откат к снапшоту любого сообщения чата
  • Никакой потери данных пользователя

Архитектура

Корневая идея — per-message snapshots. Каждое сообщение AI создаёт точку отката, в которой хранится содержимое всех изменённых файлов на момент ДО применения изменений.

type MessageSnapshot = {
  messageId: string;
  timestamp: number;
  files: Map<string, FileSnapshot>;
};

Когда AI хочет изменить файл, движок:

  1. Создаёт snapshot оригинального содержимого (если ещё нет в текущем сообщении)
  2. Считает diff и разбивает на блоки
  3. Рендерит inline-decoration в редакторе
  4. Ждёт user action

Inline decorations

В VS Code нет нативного API для "виртуального" diff внутри активного документа. Есть DecorationProvider — но он умеет только подсвечивать строки. Diff с зачёркнутыми и добавленными строками рисуется собственной системой поверх существующего текста: добавленные строки вставляются как реальные строки в TextDocument с пометкой "pending", удалённые подсвечиваются как red overlay с line-through.

При accept — pending снимается, оригинал заменяется. При reject — pending удаляется, оригинал восстанавливается из snapshot.

Rollback

Самая интересная часть — откат к произвольному сообщению. UI показывает на каждом AI-сообщении кнопку "Restore". При клике движок:

  1. Берёт snapshot этого сообщения
  2. Восстанавливает все файлы из snapshot
  3. Удаляет все сообщения после
  4. Очищает текущие inline-diff

Это позволяет пользователю экспериментировать смело: если AI пошёл не туда — один клик и вы там, где были.

Тесты

217 unit-тестов покрывают:

  • Создание snapshot при первом изменении файла в сообщении
  • Не дублирование snapshot при повторном изменении в том же сообщении
  • Корректность accept/reject по блокам
  • Восстановление при rollback
  • Edge cases с конкурентными изменениями

Что дальше

Сейчас работает на одном файле за раз. План — multi-file diffs с групповым accept/reject. И визуализация дерева сообщений (а не плоского списка).