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

613 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 селекторы (обязательно):**
```typescript
const tabList = useStore(s => s.editorOpenTabs, shallow);
const activeId = useStore(s => s.editorActiveTabId);
// FileTreePanel НЕ подписывается ни на content, ни на tabs
// TabBar НЕ подписывается на tree state
```
3. **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 раз
```
---