Cursor-style inline diff с откатами: snapshot-based архитектура
В 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 хочет изменить файл, движок:
- Создаёт snapshot оригинального содержимого (если ещё нет в текущем сообщении)
- Считает diff и разбивает на блоки
- Рендерит inline-decoration в редакторе
- Ждёт 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". При клике движок:
- Берёт snapshot этого сообщения
- Восстанавливает все файлы из snapshot
- Удаляет все сообщения после
- Очищает текущие inline-diff
Это позволяет пользователю экспериментировать смело: если AI пошёл не туда — один клик и вы там, где были.
Тесты
217 unit-тестов покрывают:
- Создание snapshot при первом изменении файла в сообщении
- Не дублирование snapshot при повторном изменении в том же сообщении
- Корректность accept/reject по блокам
- Восстановление при rollback
- Edge cases с конкурентными изменениями
Что дальше
Сейчас работает на одном файле за раз. План — multi-file diffs с групповым accept/reject. И визуализация дерева сообщений (а не плоского списка).