agent-ecosystem/docs/iterations/edit-project/plan-iterations.md
iliya 0c2f70b2b2 feat: implement in-app project editor with CodeMirror integration
- Added architectural plan and iteration plan for the in-app project editor.
- Introduced new components for the editor, including CodeEditorOverlay, FileTreePanel, and EditorTabsPanel.
- Established state management using Zustand for editor state persistence.
- Implemented IPC channels for file operations and editor functionality.
- Enhanced TeamDetailView with a button to open the editor overlay.
- Conducted reuse analysis for existing components to optimize codebase integration.
2026-02-27 22:36:06 +02:00

44 KiB
Raw Blame History

Plan: In-App Project Editor -- Iteration Plan

Контекст и предпосылки

На странице деталей команды (TeamDetailView.tsx) рядом с путём проекта (строки 761-769 в TeamDetailView.tsx, используется FolderOpen иконка и formatProjectPath()) добавляется кнопка "Open in Editor", которая открывает полноэкранный оверлей с файловым деревом, CodeMirror-редактором, вкладками и файловыми операциями.

Существующие паттерны, на которые опираемся

  1. Fullscreen overlay: ChangeReviewDialog.tsx -- полноэкранный fixed inset-0 z-50 компонент с хедером, левой панелью (ReviewFileTree) и правой панелью (ContinuousScrollView). Это точный архитектурный прототип.

  2. File tree: ReviewFileTree.tsx -- дерево файлов с buildTree(), collapse/expand, активный элемент. Будет адаптирован для файлового браузера (не review).

  3. CodeMirror: уже установлен в проекте (@codemirror/* ~20 пакетов), используется в CodeMirrorDiffView.tsx. Функция getSyncLanguageExtension() уже мапит расширения на языковые пакеты. Тема diffTheme использует CSS-переменные проекта.

  4. IPC-паттерн: module-level state + initialize/register/remove тройка + wrapHandler<T>() для IpcResult. Ближайший пример: review.ts.

  5. Preload bridge: invokeIpcWithResult<T>() для IpcResult, прямой ipcRenderer.invoke() для остальных. Группировка методов через sub-объект (как review: ReviewAPI).

  6. Path security: validateFilePath() из pathValidation.ts -- проверяет путь на sensitive patterns и sandbox.

  7. Store: Zustand slices с паттерном data/selectedId/loading/error.


Итерация 1: Walking Skeleton (файловое дерево + read-only просмотр)

Цель

Минимальный end-to-end вертикальный срез: кнопка "Open in Editor" на TeamDetailView открывает полноэкранный оверлей, где слева -- дерево файлов проекта, справа -- содержимое выбранного файла (read-only, с подсветкой синтаксиса через CodeMirror).

Зависимости (npm)

Никаких новых -- все CodeMirror-пакеты и lucide-react иконки уже установлены.

IPC каналы (новые)

Канал Направление Описание
editor:readDir renderer -> main Рекурсивное чтение директории (возвращает дерево)
editor:readFile renderer -> main Чтение содержимого файла по абсолютному пути

Новые файлы

Файл Описание
src/shared/types/editor.ts Типы: EditorTreeNode, EditorFileContent, запросы/ответы
src/main/services/editor/ProjectFileService.ts Сервис: чтение директорий (рекурсивно с лимитами) и файлов. Использует validateFilePath() для security
src/main/ipc/editor.ts IPC handlers: editor:readDir, editor:readFile. Паттерн: module-level state + wrapEditorHandler()
src/preload/constants/ipcChannels.ts Добавить EDITOR_READ_DIR, EDITOR_READ_FILE
src/renderer/components/team/editor/ProjectEditorOverlay.tsx Главный fullscreen overlay (по образцу ChangeReviewDialog.tsx)
src/renderer/components/team/editor/EditorFileTree.tsx Компонент дерева файлов (адаптация ReviewFileTree.tsx для filesystem -- без review-статусов)
src/renderer/components/team/editor/EditorCodeView.tsx Read-only CodeMirror view (адаптация CodeMirrorDiffView.tsx без merge mode)

Изменения в существующих файлах

Файл Изменение
src/shared/types/api.ts Добавить EditorAPI интерфейс + editor: EditorAPI в ElectronAPI
src/preload/index.ts Добавить editor: группу в electronAPI объект
src/main/ipc/handlers.ts Добавить initialize/register/removeEditorHandlers
src/renderer/components/team/TeamDetailView.tsx Кнопка "Open in Editor" рядом с projectPath (строка ~770), state для open/close оверлея

Важные решения

  • Security: ProjectFileService ОБЯЗАН использовать validateFilePath(filePath, projectRoot) для каждого запроса. Путь должен быть внутри projectRoot (sandbox). Нельзя читать файлы вне проекта.
  • Лимиты: readDir рекурсия ограничена глубиной (max 10 уровней) и количеством файлов (max 5000 nodes). Исключаются node_modules, .git, dist, build, __pycache__, .next.
  • Read-only: на этой итерации CodeMirror создаётся с EditorState.readOnly.of(true).
  • Lazy loading дерева: первый вызов readDir возвращает только верхний уровень. При раскрытии папки -- повторный вызов для поддиректории (ленивая загрузка). Или: полное дерево сразу, но с лимитом глубины и ignored patterns.

Тестирование

  • Unit: ProjectFileService -- чтение директории с mock fs, проверка security (reject paths outside projectRoot), проверка исключения node_modules.
  • Unit: EditorFileTree -- snapshot тесты рендеринга дерева.
  • Manual: открыть TeamDetailView, нажать "Open in Editor", убедиться что дерево загружается, клик по файлу показывает содержимое с подсветкой.

Критерии готовности

  • Кнопка видна на TeamDetailView рядом с путём проекта
  • Оверлей открывается по клику, закрывается по Escape или X
  • Дерево файлов загружается для projectPath команды
  • Клик по файлу показывает содержимое с синтаксической подсветкой
  • Попытка прочитать файл за пределами проекта -- отказ
  • pnpm typecheck проходит

Надёжность решения: 8/10

Уверенность: 9/10


Итерация 2: Editable CodeMirror + сохранение файлов

Цель

Переключить CodeMirror из read-only в редактируемый режим. Добавить Cmd+S для сохранения. Показывать индикатор unsaved changes.

IPC каналы (новые)

Канал Направление Описание
editor:writeFile renderer -> main Запись содержимого файла на диск

Новые файлы

Файл Описание
src/renderer/components/team/editor/EditorTabBar.tsx Панель вкладок (один файл пока, но подготовка к multi-tab)
src/renderer/components/team/editor/useEditorState.ts Хук для управления состоянием открытых файлов, dirty flags, save
src/renderer/store/slices/editorSlice.ts Zustand slice: openFiles, activeFilePath, dirtyFiles, loading/error

Изменения в существующих файлах

Файл Изменение
src/shared/types/editor.ts Добавить типы для write request/response
src/shared/types/api.ts Добавить writeFile в EditorAPI
src/main/services/editor/ProjectFileService.ts Метод writeFile(projectRoot, filePath, content) с validation
src/main/ipc/editor.ts Handler editor:writeFile
src/preload/index.ts Добавить editor.writeFile
src/preload/constants/ipcChannels.ts EDITOR_WRITE_FILE
src/renderer/components/team/editor/ProjectEditorOverlay.tsx Интеграция EditorTabBar, переключение read-only -> editable
src/renderer/components/team/editor/EditorCodeView.tsx Убрать readOnly, добавить onChange callback, Cmd+S keymap
src/renderer/store/index.ts Подключить editorSlice
src/renderer/store/types.ts Расширить AppState типом editorSlice

Важные решения

  • Cmd+S: перехватывается через CodeMirror keymap extension (не глобальный listener), чтобы не конфликтовать с другими горячими клавишами.
  • Dirty flag: отслеживается через сравнение текущего содержимого с оригинальным (при загрузке). Точка в названии вкладки для dirty файлов.
  • Confirm on close: если есть unsaved changes -- confirm() через существующий ConfirmDialog.
  • Backup: перед записью -- никакого backup на этой итерации (файл просто перезаписывается). В будущем можно добавить.
  • Concurrency: если файл изменился на диске пока был открыт -- пока не обрабатываем (это итерация 4-5).

Тестирование

  • Unit: ProjectFileService.writeFile -- запись с mock fs, reject для файлов вне проекта.
  • Unit: editorSlice -- открытие/закрытие файлов, dirty state, сохранение.
  • Unit: useEditorState -- хук тестирование с Zustand store.
  • Manual: открыть файл, отредактировать, Cmd+S, убедиться что файл записался, dirty индикатор сбрасывается.

Критерии готовности

  • Файл редактируется в CodeMirror (не read-only)
  • Cmd+S сохраняет файл
  • Dirty indicator (точка) на вкладке
  • При закрытии с unsaved changes -- confirmation dialog
  • Сохранение отказывает для файлов вне projectRoot

Надёжность решения: 7/10

Уверенность: 8/10


Итерация 3: Multi-tab + создание/удаление файлов

Цель

Поддержка нескольких открытых файлов во вкладках. Контекстное меню на файловом дереве: создать файл, создать папку, удалить файл. Переименование -- вне scope.

IPC каналы (новые)

Канал Направление Описание
editor:createFile renderer -> main Создать файл (с опциональным начальным содержимым)
editor:createDir renderer -> main Создать директорию
editor:deleteFile renderer -> main Удалить файл (в Trash через Electron shell.trashItem)

Новые файлы

Файл Описание
src/renderer/components/team/editor/EditorContextMenu.tsx Context menu для дерева файлов (New File, New Folder, Delete, Reveal in Finder)
src/renderer/components/team/editor/NewFileDialog.tsx Маленький inline-input для ввода имени нового файла/папки

Изменения в существующих файлах

Файл Изменение
src/shared/types/editor.ts Типы для create/delete запросов
src/shared/types/api.ts Расширить EditorAPI методами createFile, createDir, deleteFile
src/main/services/editor/ProjectFileService.ts Методы createFile, createDir, deleteFile. deleteFile использует shell.trashItem() (безопасное удаление)
src/main/ipc/editor.ts 3 новых handler
src/preload/index.ts 3 новых метода в editor
src/preload/constants/ipcChannels.ts EDITOR_CREATE_FILE, EDITOR_CREATE_DIR, EDITOR_DELETE_FILE
src/renderer/components/team/editor/EditorTabBar.tsx Multi-tab: массив вкладок, переключение, close (X), close other tabs, middle-click close
src/renderer/components/team/editor/EditorFileTree.tsx Right-click context menu, refresh после create/delete
src/renderer/components/team/editor/ProjectEditorOverlay.tsx Управление массивом открытых файлов, переключение активной вкладки
src/renderer/store/slices/editorSlice.ts Массив openTabs, activeTabId, actions: openFile, closeFile, switchTab, reorderTabs

Важные решения

  • Удаление через Trash: используем shell.trashItem() (Electron API) вместо fs.unlink(). Это безопасно -- пользователь может восстановить файл из корзины.
  • Confirm on delete: обязательный ConfirmDialog перед удалением.
  • Tab ordering: drag-and-drop для вкладок через @dnd-kit (уже установлен в проекте).
  • Имя нового файла: валидация -- запрет на ., .., / в начале, запрет на спецсимволы.
  • Refresh дерева: после create/delete автоматически перечитываем поддерево. Не нужен FileWatcher -- явный refresh.

Тестирование

  • Unit: ProjectFileService.createFile/deleteFile с mock fs.
  • Unit: editorSlice -- multi-tab actions (open, close, reorder).
  • Unit: EditorContextMenu -- рендеринг, клики.
  • Manual: открыть несколько файлов, переключаться между вкладками, создать файл, удалить файл.

Критерии готовности

  • Можно открыть несколько файлов одновременно
  • Вкладки переключаются, закрываются
  • Правый клик по дереву -- New File, New Folder, Delete
  • Создание файла добавляет его в дерево
  • Удаление -- через Trash с confirmation

Надёжность решения: 7/10

Уверенность: 8/10


Итерация 4: Горячие клавиши, поиск, UX polish

Цель

Клавиатурная навигация (Cmd+P quick open, Cmd+W close tab, Cmd+Shift+[ / ] switch tabs). Поиск по содержимому файлов через Cmd+Shift+F. Breadcrumb навигация. Иконки файлов по типу.

IPC каналы (новые)

Канал Направление Описание
editor:searchInFiles renderer -> main Поиск по содержимому файлов (grep-like)

Новые файлы

Файл Описание
src/renderer/components/team/editor/QuickOpenDialog.tsx Cmd+P dialog: fuzzy search по именам файлов (по образцу cmdk -- уже установлен)
src/renderer/components/team/editor/SearchInFilesPanel.tsx Панель результатов поиска (заменяет или дополняет file tree)
src/renderer/components/team/editor/EditorBreadcrumb.tsx Breadcrumb навигация по пути текущего файла
src/renderer/components/team/editor/fileIcons.ts Маппинг расширений на lucide-react иконки и цвета
src/renderer/hooks/useEditorKeyboardShortcuts.ts Хук для всех горячих клавиш редактора
src/main/services/editor/FileSearchService.ts Сервис: search in files с лимитами (grep-like, max 100 results)

Изменения в существующих файлах

Файл Изменение
src/shared/types/editor.ts Типы для search request/response
src/shared/types/api.ts searchInFiles в EditorAPI
src/main/ipc/editor.ts Handler editor:searchInFiles
src/preload/index.ts editor.searchInFiles
src/preload/constants/ipcChannels.ts EDITOR_SEARCH_IN_FILES
src/renderer/components/team/editor/ProjectEditorOverlay.tsx Интеграция QuickOpen, SearchInFiles, Breadcrumb, keyboard shortcuts
src/renderer/components/team/editor/EditorFileTree.tsx Иконки файлов по типу
src/renderer/components/team/editor/EditorTabBar.tsx Иконки файлов на вкладках

Важные решения

  • Quick Open: использовать cmdk (уже в зависимостях, v1.0.4) для fuzzy search по именам файлов. Список файлов загружается при открытии оверлея.
  • Search in Files: серверная сторона делает простой grep по файлам с Node.js (readline + regex). Не используем external tools типа ripgrep -- держим zero-dependency. Лимит: 100 результатов, max 10MB на файл.
  • Горячие клавиши: Cmd+P (quick open), Cmd+W (close tab), Cmd+S (save), Cmd+Shift+F (search), Cmd+Shift+[ / ] (switch tabs), Cmd+\ (toggle file tree).
  • Breadcrumb: кликабельный -- каждый сегмент пути открывает эту папку в дереве.

Тестирование

  • Unit: FileSearchService -- поиск по mock файлам, лимиты.
  • Unit: useEditorKeyboardShortcuts -- обработка горячих клавиш.
  • Unit: fileIcons.ts -- маппинг расширений.
  • Manual: Cmd+P, Cmd+Shift+F, навигация клавиатурой.

Критерии готовности

  • Cmd+P открывает quick open с fuzzy search
  • Cmd+Shift+F показывает результаты поиска по содержимому
  • Все основные горячие клавиши работают
  • Breadcrumb-навигация для текущего файла
  • Иконки файлов по типу в дереве и вкладках

Надёжность решения: 7/10

Уверенность: 7/10


Итерация 5: Git status, file watching, расширенные возможности

Цель

Показывать git status (modified/untracked/staged) в дереве файлов. Live refresh при изменениях на диске. Conflict detection при сохранении. Minimap. Line numbers toggle.

IPC каналы (новые)

Канал Направление Описание
editor:gitStatus renderer -> main Получить git status для директории (modified, staged, untracked)
editor:watchDir renderer -> main Запустить file watcher для проекта (возвращает cleanup)
editor:change main -> renderer Event: файл изменился на диске

Новые файлы

Файл Описание
src/main/services/editor/EditorFileWatcher.ts FileWatcher адаптация (~60 LOC) для отслеживания изменений в projectRoot
src/main/services/editor/GitStatusService.ts Сервис: вызывает git status --porcelain и парсит вывод
src/renderer/components/team/editor/GitStatusBadge.tsx Бейджи M/U/A рядом с файлами в дереве

Изменения в существующих файлах

Файл Изменение
src/shared/types/editor.ts GitFileStatus, EditorFileChangeEvent
src/shared/types/api.ts gitStatus, onEditorFileChange в EditorAPI
src/main/ipc/editor.ts Handlers для git status и file watcher events
src/preload/index.ts editor.gitStatus, editor.onFileChange
src/preload/constants/ipcChannels.ts EDITOR_GIT_STATUS, EDITOR_WATCH_DIR, EDITOR_CHANGE
src/renderer/components/team/editor/EditorFileTree.tsx Git status badges (M = modified, U = untracked, A = staged)
src/renderer/components/team/editor/EditorCodeView.tsx Line wrapping toggle, conflict detection при сохранении
src/renderer/components/team/editor/ProjectEditorOverlay.tsx File watcher подписка, auto-refresh дерева, conflict modal при concurrent edit
src/renderer/store/slices/editorSlice.ts Git status data, file watcher state

Важные решения

  • Git status: вызываем child_process с git status --porcelain -u в projectRoot. Парсим вывод. Кешируем на 5 секунд. Не используем libgit2 -- слишком тяжёлый.
  • File watcher: используем существующий chokidar-подобный паттерн (как FileWatcher в проекте). Debounce 200ms. При получении события -- refresh дерева и уведомление если открытый файл изменился.
  • Conflict detection: при сохранении -- проверить mtime файла. Если изменился после последнего чтения -- показать conflict dialog (перезаписать / отменить / diff).
  • Minimap: CodeMirror не имеет встроенного minimap. Можно использовать @replit/codemirror-minimap или пропустить. Решение: пропустить minimap (слишком специфичная dependency), вместо этого добавить line wrap toggle и go-to-line (Cmd+G).

Тестирование

  • Unit: GitStatusService -- парсинг git status --porcelain вывода.
  • Unit: EditorFileWatcher -- debounce, event types.
  • Unit: conflict detection логика.
  • Manual: изменить файл в внешнем редакторе, убедиться что отображается conflict.

Критерии готовности

  • Git status бейджи в файловом дереве
  • Auto-refresh при изменениях на диске
  • Conflict detection при сохранении файла, изменённого извне
  • Go-to-line (Cmd+G)
  • Line wrap toggle

Надёжность решения: 6/10

Уверенность: 7/10


Сводная таблица файлов по итерациям

Итерация 1 (7 новых, 4 изменения)

Новые: shared/types/editor.ts, main/services/editor/ProjectFileService.ts, main/ipc/editor.ts, renderer/components/team/editor/ProjectEditorOverlay.tsx, renderer/components/team/editor/EditorFileTree.tsx, renderer/components/team/editor/EditorCodeView.tsx Изменения: shared/types/api.ts, preload/index.ts, preload/constants/ipcChannels.ts, main/ipc/handlers.ts, renderer/components/team/TeamDetailView.tsx

Итерация 2 (3 новых, ~8 изменений)

Новые: renderer/components/team/editor/EditorTabBar.tsx, renderer/hooks/useEditorState.ts, renderer/store/slices/editorSlice.ts Изменения: shared/types/editor.ts, shared/types/api.ts, main/services/editor/ProjectFileService.ts, main/ipc/editor.ts, preload/index.ts, preload/constants/ipcChannels.ts, renderer/components/team/editor/*, renderer/store/index.ts, renderer/store/types.ts

Итерация 3 (2 новых, ~8 изменений)

Новые: renderer/components/team/editor/EditorContextMenu.tsx, renderer/components/team/editor/NewFileDialog.tsx Изменения: многие файлы из итерации 2

Итерация 4 (6 новых, ~8 изменений)

Новые: QuickOpenDialog.tsx, SearchInFilesPanel.tsx, EditorBreadcrumb.tsx, fileIcons.ts, useEditorKeyboardShortcuts.ts, main/services/editor/FileSearchService.ts

Итерация 5 (3 новых, ~7 изменений)

Новые: EditorFileWatcher.ts, GitStatusService.ts, GitStatusBadge.tsx


Риски и предупреждения

  1. Безопасность (критичный риск): каждый файловый IPC handler ОБЯЗАН валидировать что запрашиваемый путь находится внутри projectRoot. Path traversal (../../etc/passwd) -- главный вектор атаки. Используем существующий validateFilePath() из src/main/utils/pathValidation.ts (НЕ писать свой).

  2. Большие проекты: дерево файлов может содержать тысячи файлов. Обязательны excluded patterns (node_modules, .git) и лимиты. Для поиска по файлам -- лимит на размер файла.

  3. Race conditions при сохранении: если агент Claude параллельно редактирует тот же файл -- потеря данных. Итерация 5 добавляет mtime-проверку, но полноценный lock отсутствует.

  4. Memory: CodeMirror для очень больших файлов (10MB+) может потреблять много памяти. Лимит на размер читаемого файла: 2MB (не 5MB -- снижено после security review; IPC сериализация удваивает потребление памяти).

  5. ProseMirror vs CodeMirror: в requirements указан ProseMirror, но в проекте уже глубоко интегрирован CodeMirror (20+ пакетов, diff view, языковые пакеты). Рекомендация: использовать CodeMirror (не ProseMirror). ProseMirror ориентирован на rich-text, а CodeMirror -- на код. CodeMirror 6 = тот же автор (Marijn Haverbeke), уже в проекте, zero additional dependencies.


Архитектурные решения после ревизии

Добавлено после ревизии. Влияет на каждую итерацию.

Обязательные рефакторинги ДО или ВО ВРЕМЯ итерации 1

  1. Извлечь buildTree() в src/renderer/utils/fileTreeBuilder.ts (из ReviewFileTree.tsx). Иначе будет дублирование при создании EditorFileTree. Рефакторинг не ломает Review -- это extract-and-import.

  2. Извлечь getSyncLanguageExtension() + getAsyncLanguageDesc() в src/renderer/utils/codemirrorLanguages.ts (из CodeMirrorDiffView.tsx). Аналогично -- extract-and-import, CodeMirrorDiffView начинает импортировать из утилиты.

  3. Извлечь базовую тему CM в src/renderer/utils/codemirrorTheme.ts (из diffTheme в CodeMirrorDiffView.tsx). Общие стили (&, .cm-gutters, .cm-scroller, .cm-content, .cm-cursor, .cm-selectionBackground) -- в общую тему. Diff-специфичные (.cm-changedLine, .cm-deletedChunk и т.д.) -- остаются в CodeMirrorDiffView.tsx.

  4. Извлечь wrapHandler в src/main/ipc/ipcWrapper.ts (из review.ts). createIpcWrapper('IPC:editor') вместо копирования wrapReviewHandler.

  5. Имя сервиса: ProjectFileService (не FileEditorService). Stateless, без rootPath в конструкторе. Каждый метод принимает projectRoot как первый аргумент. Паттерн: TeamDataService.

Изменения в итерациях по результатам ревизии

Итерация 1:

  • EditorFileTree.tsx использует generic FileTree из fileTreeBuilder.ts + render-prop для иконок
  • EditorCodeView.tsx использует extracted codemirrorLanguages.ts и codemirrorTheme.ts
  • ProjectFileService -- stateless, readDir(projectRoot, dirPath), readFile(projectRoot, filePath)
  • Security: validateFilePath() из pathValidation.ts, НЕ свой assertInsideRoot()
  • НЕ создавать editorSlice на итерации 1 -- state для read-only просмотра можно держать в useState

Итерация 2:

  • editorSlice.ts создаётся с чёткими секциями-группами (tree / tabs / content-save)
  • buildEditorExtensions(options) -- фабрика extensions, компонент не знает о конкретных CM plugins
  • useEditorState.ts -> убрать. Логика целиком в slice. Хук useEditorState дублирует slice.

Итерация 3:

  • Tab management actions (openFile, closeTab, setActiveTab) уже в slice с итерации 2
  • EditorContextMenu.tsx -- ОК, отдельный компонент
  • NewFileDialog.tsx -- ОК, inline input

Итерация 4:

  • FileSearchService.ts -- отдельный сервис в main, ОК (SRP)
  • useEditorKeyboardShortcuts.ts -- ОК, отдельный хук
  • fileIcons.ts -- ОК, чистая утилита

Итерация 5:

  • GitStatusService.ts -- отдельный сервис, ОК
  • EditorFileWatcher.ts -- повторяет паттерн FileWatcher (~60 LOC), ОК
  • mtime conflict detection -- необходима проверка и в saveFile (slice), и в writeFile (service)

UX Review

Добавлено после UX-ревью. Дополнения и исправления по итерациям.

Дополнения к итерации 1 (Walking Skeleton)

  1. Focus management: При открытии overlay -- фокус на первый файл в дереве. При закрытии -- вернуть фокус на кнопку "Open in Editor" (паттерн returnFocusRef). Добавить inert на фоновый контент.

  2. ARIA: File tree сразу с role="tree", role="treeitem", aria-expanded, role="group". Не откладывать accessibility на потом.

  3. Пустой проект: Если readDir возвращает 0 видимых файлов -- показать "No files found" + "Create a new file?" (кнопка неактивна до итерации 3).

  4. Binary файлы: Уже на итерации 1 (read-only) нужна проверка бинарности. Добавить isBinary в ReadFileResult и EditorBinaryState.tsx -- "This file is binary. Open in system viewer?"

  5. Глубокая вложенность: Max визуальный indent = 12 уровней. Tooltip с полным путём на глубоких узлах.

Дополнения к итерации 2 (Editing + Save)

  1. Status bar: Добавить EditorStatusBar.tsx -- [Ln 42, Col 15] | [TypeScript] | [Spaces: 2]. Данные из CM6 state. CSS: bg-surface-sidebar border-t border-border text-text-muted text-xs h-6.

  2. Unsaved changes при закрытии overlay: Не только при закрытии tab, но и при Escape/X для overlay. Три кнопки: "Save All & Close" / "Discard & Close" / "Cancel". Добавить hasUnsavedChanges() в slice.

  3. Файл удалён извне: При saveFile с ENOENT -- inline-ошибка "File was deleted. Create new? / Close tab". Не падать.

Дополнения к итерации 3 (Multi-tab + file ops)

  1. Disambiguation tab labels: Два таба "index.ts" -- нужно показать "(main/utils)" и "(renderer/utils)". Утилита getDisambiguatedTabLabel() в src/renderer/utils/tabLabelDisambiguation.ts.

  2. Длинные имена файлов: Табы с max-width ~160px, truncate, tooltip. Modified dot ПЕРЕД текстом (не обрезается при truncate).

  3. ARIA для tab bar: role="tablist", role="tab", aria-selected, role="tabpanel".

Исправления к итерации 4 (Hotkeys, search, UX polish)

  1. Keyboard shortcuts -- конфликт: Cmd+[ / Cmd+] это indent/outdent в CM6 и VS Code! Переключение табов: Cmd+Shift+[ / Cmd+Shift+] (VS Code convention). Добавить Ctrl+Tab / Ctrl+Shift+Tab.

  2. Cmd+B toggle sidebar: Добавить в список горячих клавиш. Sidebar width persist в localStorage.

  3. Cmd+G go to line: Добавить. CM6 уже поддерживает через gotoLine command.

  4. Discoverability: Кнопка ? в header overlay (как в ChangeReviewDialog). EmptyState показывает шпаргалку shortcuts.

Дополнения к итерации 5 (Git, file watching)

  1. File changed on disk while open in tab: При обнаружении изменения -- banner в табе: "File changed on disk. [Reload] [Keep mine] [Show diff]". Не перезаписывать молча.

  2. File deleted on disk while open in tab: Banner: "File no longer exists on disk. [Close tab]". Не показывать ошибку при попытке сохранить -- показать предупреждение.


Security Review -- дополнения по итерациям

Полный аудит безопасности описан в plan-architecture.md секция 18. Здесь -- конкретные требования для каждой итерации.

Итерация 1: Security-critical

  1. ProjectFileService.readDir(): Валидировать КАЖДЫЙ entry через validateFilePath(). Для symlinks -- fs.realpath() + повторная проверка containment. Молча пропускать symlinks, ведущие за пределы projectRoot (см. SEC-2 в plan-architecture.md).

  2. ProjectFileService.readFile(): Проверить fs.lstat() -> isFile() ДО чтения. Проверить stats.size <= 2MB. Блокировать пути /dev/, /proc/, /sys/. После чтения -- post-read verify через fs.realpath() (TOCTOU mitigation).

  3. projectRoot: Хранить в module-level state editor.ts, НЕ принимать от renderer при каждом IPC вызове. Устанавливать через editor:open(projectPath) с валидацией.

  4. Sensitive файлы: validateFilePath() уже блокирует .env, .ssh, credentials.json и т.д. В readDir: показывать с пометкой "locked", при клике -- "Sensitive file, cannot open".

Итерация 2: Security-critical

  1. ProjectFileService.writeFile():

    • validateFilePath() ДО записи
    • Buffer.byteLength(content, 'utf8') <= 2MB ДО записи
    • Atomic write: tmp файл в той же директории + rename()
    • Запрет записи в .git/ директорию
    • Post-write verify не нужна (atomic rename -- одна операция)
  2. editor:writeFile IPC handler: Параметр filePath от renderer валидируется через validateFilePath(filePath, activeProjectRoot). activeProjectRoot из module-level state.

Итерация 3: Security-critical

  1. editor:createFile: Валидация имени файла через validateFileName():

    • Запрет . и .. как имени
    • Запрет control characters (\x00-\x1f)
    • Запрет path separators (/, \, :)
    • Запрет NUL bytes
    • Max длина 255 символов
    • Запрет sensitive паттернов (.env, *.key) при СОЗДАНИИ (опционально -- можно разрешить)
  2. editor:deleteFile: Использовать shell.trashItem(), НЕ fs.unlink(). Валидация пути через validateFilePath().

  3. Валидация parentDir: При createFile(parentDir, name) -- валидировать и parentDir, и path.join(parentDir, name).

Итерация 4: Security-critical

  1. editor:searchInFiles:
    • ТОЛЬКО literal string search, НЕ regex от пользователя
    • Max 1000 файлов для поиска, max 1MB на файл
    • Запустить в worker_thread или с AbortController timeout
    • Каждый файл для поиска валидировать через validateFilePath()

Итерация 5: Medium security

  1. editor:gitStatus: Выполняет child_process.exec('git status') -- убедиться что cwd установлен в projectRoot и что projectRoot валиден.

  2. editor:watchDir: FileWatcher на projectRoot -- ОК, но при получении событий не передавать полные пути файлов в renderer без валидации.

  3. editor:change events (main->renderer): Пути файлов в events -- потенциальная утечка информации если watcher случайно поймает файл за пределами проекта (через symlink).

ВНИМАНИЕ: Существующая уязвимость (не связана с editor)

review:saveEditedFile в src/main/ipc/review.ts записывает файл без валидации пути. См. SEC-11 в plan-architecture.md. Необходим отдельный hotfix НЕЗАВИСИМО от editor-фичи.


Performance Review -- дополнения по итерациям

Полный аудит в plan-architecture.md секция 19. Здесь -- конкретные performance-требования для каждой итерации.

Итерация 1: Performance-critical

  1. EditorView lifecycle (CRITICAL): НЕ использовать Map<tabId, EditorView> + CSS show/hide (как описано в plan-architecture секция 6.5). Использовать EditorState pooling: Map<tabId, EditorState> в useRef + один активный EditorView. При переключении таба: savedStates.set(oldId, view.state) -> view.destroy() -> new EditorView({ state: savedStates.get(newId) }). Паттерн initialState уже используется в CodeMirrorDiffView.tsx (строки 699-705).

  2. readDir лимиты: MAX_ENTRIES_PER_DIR = 500 (не 10,000). При превышении -- "N more files..." + кнопка "Show all". Только root level при открытии, expand = depth=1 для конкретной папки.

  3. readFile тиерная стратегия: <256KB мгновенно | 256KB-2MB с progress | 2MB-5MB preview only (100 строк) | >5MB external editor. Детектировать минификацию (строка >10,000 chars) и binary (null bytes в первых 8KB).

  4. Дедупликация IPC: Map<string, Promise<ReadFileResult>> для readFile. Если файл уже загружается -- ждать результат, не создавать новый запрос.

Итерация 2: Performance-critical

  1. НЕ хранить modified content в Zustand (CRITICAL): editorModifiedContents: Record<string, string> из секции 2.1 plan-architecture -- УБРАТЬ. Контент живёт только в EditorState CodeMirror. В Zustand: editorModifiedFiles: Set<string> (только dirty flags). Dirty flag обновляется debounced (300ms) через EditorView.updateListener (паттерн из CodeMirrorDiffView строки 517-527).

  2. Гранулярные Zustand селекторы (обязательно):

const tabList = useStore(s => s.editorOpenTabs, shallow);
const activeId = useStore(s => s.editorActiveTabId);
// FileTreePanel НЕ подписывается ни на content, ни на tabs
// TabBar НЕ подписывается на tree state
  1. LRU eviction EditorState: При >30 states в кеше -- вытеснять oldest, сохраняя { content: string, cursorPos: number } (без undo). При возврате к вытесненному табу -- восстановить через EditorState.create().

Итерация 3: Performance-medium

  1. Tab closing -- memory cleanup: При closeTab: stateCache.delete(tabId). При closeAllTabs: stateCache.clear(). Явно вызывать -- не полагаться на GC.

  2. Concurrent file operations: При createFile/deleteFile -- дебаунсить обновление дерева (500ms), не перечитывать после каждой операции.

Итерация 4: Performance-critical

  1. File tree виртуализация (HIGH): Перейти на @tanstack/react-virtual (уже в проекте -- DateGroupedSessions.tsx, ChatHistory.tsx, NotificationsView.tsx). flattenTree(tree, expandedDirs) -> FlatNode[] + useVirtualizer({ count, estimateSize: () => 28 }). Рендерить только видимые ноды.

  2. Search in files -- main process: Запускать в worker_thread или с AbortController (timeout 5s). Limit: 100 результатов, max 1MB на файл. НЕ читать binary файлы для поиска.

  3. Quick Open (Cmd+P): Кешировать flat file list при открытии editor. НЕ перечитывать на каждое открытие Cmd+P. Invalidate по F5 или file watcher event.

Итерация 5: Performance-medium

  1. File watcher -- opt-in: НЕ включать по умолчанию. Toggle "Watch for external changes". По умолчанию -- ручной refresh (F5). При включении: fs.watch({ recursive: true }) с фильтрацией (node_modules/.git/dist) и debounce 200ms.

  2. Git status -- кеширование: Результат git status --porcelain кешировать на 5 секунд (как в плане). При file watcher event -- invalidate и перечитать.

Benchmarks для CI/Manual

Benchmark 1: EditorView memory
  Открыть 25 файлов x 200KB
  Измерить: performance.memory.usedJSHeapSize
  Порог: < 150MB

Benchmark 2: Tab switch latency
  Переключить таб (500KB файл с syntax highlighting)
  Измерить: time from click to contentful paint
  Порог: < 50ms

Benchmark 3: File tree render
  5000+ файлов, все папки раскрыты (с виртуализацией)
  Измерить: FPS при скролле
  Порог: >= 55fps

Benchmark 4: readDir latency
  Директория с 5000 файлами
  Измерить: time from click to tree displayed
  Порог: < 200ms

Benchmark 5: Keystroke re-renders
  React DevTools Profiler при наборе текста
  Порог: FileTreePanel и TabBar рендерятся 0 раз