- 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.
44 KiB
Plan: In-App Project Editor -- Iteration Plan
Контекст и предпосылки
На странице деталей команды (TeamDetailView.tsx) рядом с путём проекта (строки 761-769 в TeamDetailView.tsx, используется FolderOpen иконка и formatProjectPath()) добавляется кнопка "Open in Editor", которая открывает полноэкранный оверлей с файловым деревом, CodeMirror-редактором, вкладками и файловыми операциями.
Существующие паттерны, на которые опираемся
-
Fullscreen overlay:
ChangeReviewDialog.tsx-- полноэкранныйfixed inset-0 z-50компонент с хедером, левой панелью (ReviewFileTree) и правой панелью (ContinuousScrollView). Это точный архитектурный прототип. -
File tree:
ReviewFileTree.tsx-- дерево файлов сbuildTree(), collapse/expand, активный элемент. Будет адаптирован для файлового браузера (не review). -
CodeMirror: уже установлен в проекте (
@codemirror/*~20 пакетов), используется вCodeMirrorDiffView.tsx. ФункцияgetSyncLanguageExtension()уже мапит расширения на языковые пакеты. ТемаdiffThemeиспользует CSS-переменные проекта. -
IPC-паттерн: module-level state +
initialize/register/removeтройка +wrapHandler<T>()для IpcResult. Ближайший пример:review.ts. -
Preload bridge:
invokeIpcWithResult<T>()для IpcResult, прямойipcRenderer.invoke()для остальных. Группировка методов через sub-объект (какreview: ReviewAPI). -
Path security:
validateFilePath()изpathValidation.ts-- проверяет путь на sensitive patterns и sandbox. -
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
Риски и предупреждения
-
Безопасность (критичный риск): каждый файловый IPC handler ОБЯЗАН валидировать что запрашиваемый путь находится внутри
projectRoot. Path traversal (../../etc/passwd) -- главный вектор атаки. Используем существующийvalidateFilePath()изsrc/main/utils/pathValidation.ts(НЕ писать свой). -
Большие проекты: дерево файлов может содержать тысячи файлов. Обязательны excluded patterns (
node_modules,.git) и лимиты. Для поиска по файлам -- лимит на размер файла. -
Race conditions при сохранении: если агент Claude параллельно редактирует тот же файл -- потеря данных. Итерация 5 добавляет mtime-проверку, но полноценный lock отсутствует.
-
Memory: CodeMirror для очень больших файлов (10MB+) может потреблять много памяти. Лимит на размер читаемого файла: 2MB (не 5MB -- снижено после security review; IPC сериализация удваивает потребление памяти).
-
ProseMirror vs CodeMirror: в requirements указан ProseMirror, но в проекте уже глубоко интегрирован CodeMirror (20+ пакетов, diff view, языковые пакеты). Рекомендация: использовать CodeMirror (не ProseMirror). ProseMirror ориентирован на rich-text, а CodeMirror -- на код. CodeMirror 6 = тот же автор (Marijn Haverbeke), уже в проекте, zero additional dependencies.
Архитектурные решения после ревизии
Добавлено после ревизии. Влияет на каждую итерацию.
Обязательные рефакторинги ДО или ВО ВРЕМЯ итерации 1
-
Извлечь
buildTree()вsrc/renderer/utils/fileTreeBuilder.ts(изReviewFileTree.tsx). Иначе будет дублирование при созданииEditorFileTree. Рефакторинг не ломает Review -- это extract-and-import. -
Извлечь
getSyncLanguageExtension()+getAsyncLanguageDesc()вsrc/renderer/utils/codemirrorLanguages.ts(изCodeMirrorDiffView.tsx). Аналогично -- extract-and-import,CodeMirrorDiffViewначинает импортировать из утилиты. -
Извлечь базовую тему 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. -
Извлечь
wrapHandlerвsrc/main/ipc/ipcWrapper.ts(изreview.ts).createIpcWrapper('IPC:editor')вместо копированияwrapReviewHandler. -
Имя сервиса:
ProjectFileService(неFileEditorService). Stateless, безrootPathв конструкторе. Каждый метод принимаетprojectRootкак первый аргумент. Паттерн:TeamDataService.
Изменения в итерациях по результатам ревизии
Итерация 1:
EditorFileTree.tsxиспользует genericFileTreeизfileTreeBuilder.ts+ render-prop для иконокEditorCodeView.tsxиспользует extractedcodemirrorLanguages.tsиcodemirrorTheme.tsProjectFileService-- 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 pluginsuseEditorState.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)
-
Focus management: При открытии overlay -- фокус на первый файл в дереве. При закрытии -- вернуть фокус на кнопку "Open in Editor" (паттерн
returnFocusRef). Добавитьinertна фоновый контент. -
ARIA: File tree сразу с
role="tree",role="treeitem",aria-expanded,role="group". Не откладывать accessibility на потом. -
Пустой проект: Если
readDirвозвращает 0 видимых файлов -- показать "No files found" + "Create a new file?" (кнопка неактивна до итерации 3). -
Binary файлы: Уже на итерации 1 (read-only) нужна проверка бинарности. Добавить
isBinaryвReadFileResultиEditorBinaryState.tsx-- "This file is binary. Open in system viewer?" -
Глубокая вложенность: Max визуальный indent = 12 уровней. Tooltip с полным путём на глубоких узлах.
Дополнения к итерации 2 (Editing + Save)
-
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. -
Unsaved changes при закрытии overlay: Не только при закрытии tab, но и при Escape/X для overlay. Три кнопки: "Save All & Close" / "Discard & Close" / "Cancel". Добавить
hasUnsavedChanges()в slice. -
Файл удалён извне: При
saveFileс ENOENT -- inline-ошибка "File was deleted. Create new? / Close tab". Не падать.
Дополнения к итерации 3 (Multi-tab + file ops)
-
Disambiguation tab labels: Два таба "index.ts" -- нужно показать "(main/utils)" и "(renderer/utils)". Утилита
getDisambiguatedTabLabel()вsrc/renderer/utils/tabLabelDisambiguation.ts. -
Длинные имена файлов: Табы с max-width ~160px,
truncate, tooltip. Modified dot ПЕРЕД текстом (не обрезается при truncate). -
ARIA для tab bar:
role="tablist",role="tab",aria-selected,role="tabpanel".
Исправления к итерации 4 (Hotkeys, search, UX polish)
-
Keyboard shortcuts -- конфликт:
Cmd+[/Cmd+]это indent/outdent в CM6 и VS Code! Переключение табов:Cmd+Shift+[/Cmd+Shift+](VS Code convention). ДобавитьCtrl+Tab/Ctrl+Shift+Tab. -
Cmd+B toggle sidebar: Добавить в список горячих клавиш. Sidebar width persist в localStorage.
-
Cmd+G go to line: Добавить. CM6 уже поддерживает через
gotoLinecommand. -
Discoverability: Кнопка
?в header overlay (как в ChangeReviewDialog). EmptyState показывает шпаргалку shortcuts.
Дополнения к итерации 5 (Git, file watching)
-
File changed on disk while open in tab: При обнаружении изменения -- banner в табе: "File changed on disk. [Reload] [Keep mine] [Show diff]". Не перезаписывать молча.
-
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
-
ProjectFileService.readDir(): Валидировать КАЖДЫЙ entry черезvalidateFilePath(). Для symlinks --fs.realpath()+ повторная проверка containment. Молча пропускать symlinks, ведущие за пределы projectRoot (см. SEC-2 в plan-architecture.md). -
ProjectFileService.readFile(): Проверитьfs.lstat()->isFile()ДО чтения. Проверитьstats.size <= 2MB. Блокировать пути/dev/,/proc/,/sys/. После чтения -- post-read verify черезfs.realpath()(TOCTOU mitigation). -
projectRoot: Хранить в module-level state
editor.ts, НЕ принимать от renderer при каждом IPC вызове. Устанавливать черезeditor:open(projectPath)с валидацией. -
Sensitive файлы:
validateFilePath()уже блокирует.env,.ssh,credentials.jsonи т.д. В readDir: показывать с пометкой "locked", при клике -- "Sensitive file, cannot open".
Итерация 2: Security-critical
-
ProjectFileService.writeFile():validateFilePath()ДО записиBuffer.byteLength(content, 'utf8') <= 2MBДО записи- Atomic write: tmp файл в той же директории +
rename() - Запрет записи в
.git/директорию - Post-write verify не нужна (atomic rename -- одна операция)
-
editor:writeFileIPC handler: ПараметрfilePathот renderer валидируется черезvalidateFilePath(filePath, activeProjectRoot).activeProjectRootиз module-level state.
Итерация 3: Security-critical
-
editor:createFile: Валидация имени файла черезvalidateFileName():- Запрет
.и..как имени - Запрет control characters (
\x00-\x1f) - Запрет path separators (
/,\,:) - Запрет NUL bytes
- Max длина 255 символов
- Запрет sensitive паттернов (
.env,*.key) при СОЗДАНИИ (опционально -- можно разрешить)
- Запрет
-
editor:deleteFile: Использоватьshell.trashItem(), НЕfs.unlink(). Валидация пути черезvalidateFilePath(). -
Валидация parentDir: При
createFile(parentDir, name)-- валидировать иparentDir, иpath.join(parentDir, name).
Итерация 4: Security-critical
editor:searchInFiles:- ТОЛЬКО literal string search, НЕ regex от пользователя
- Max 1000 файлов для поиска, max 1MB на файл
- Запустить в
worker_threadили с AbortController timeout - Каждый файл для поиска валидировать через
validateFilePath()
Итерация 5: Medium security
-
editor:gitStatus: Выполняетchild_process.exec('git status')-- убедиться чтоcwdустановлен в projectRoot и что projectRoot валиден. -
editor:watchDir: FileWatcher на projectRoot -- ОК, но при получении событий не передавать полные пути файлов в renderer без валидации. -
editor:changeevents (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
-
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). -
readDir лимиты: MAX_ENTRIES_PER_DIR = 500 (не 10,000). При превышении -- "N more files..." + кнопка "Show all". Только root level при открытии, expand = depth=1 для конкретной папки.
-
readFile тиерная стратегия: <256KB мгновенно | 256KB-2MB с progress | 2MB-5MB preview only (100 строк) | >5MB external editor. Детектировать минификацию (строка >10,000 chars) и binary (null bytes в первых 8KB).
-
Дедупликация IPC:
Map<string, Promise<ReadFileResult>>для readFile. Если файл уже загружается -- ждать результат, не создавать новый запрос.
Итерация 2: Performance-critical
-
НЕ хранить 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). -
Гранулярные Zustand селекторы (обязательно):
const tabList = useStore(s => s.editorOpenTabs, shallow);
const activeId = useStore(s => s.editorActiveTabId);
// FileTreePanel НЕ подписывается ни на content, ни на tabs
// TabBar НЕ подписывается на tree state
- LRU eviction EditorState: При >30 states в кеше -- вытеснять oldest, сохраняя
{ content: string, cursorPos: number }(без undo). При возврате к вытесненному табу -- восстановить черезEditorState.create().
Итерация 3: Performance-medium
-
Tab closing -- memory cleanup: При closeTab:
stateCache.delete(tabId). При closeAllTabs:stateCache.clear(). Явно вызывать -- не полагаться на GC. -
Concurrent file operations: При createFile/deleteFile -- дебаунсить обновление дерева (500ms), не перечитывать после каждой операции.
Итерация 4: Performance-critical
-
File tree виртуализация (HIGH): Перейти на
@tanstack/react-virtual(уже в проекте -- DateGroupedSessions.tsx, ChatHistory.tsx, NotificationsView.tsx).flattenTree(tree, expandedDirs) -> FlatNode[]+useVirtualizer({ count, estimateSize: () => 28 }). Рендерить только видимые ноды. -
Search in files -- main process: Запускать в worker_thread или с AbortController (timeout 5s). Limit: 100 результатов, max 1MB на файл. НЕ читать binary файлы для поиска.
-
Quick Open (Cmd+P): Кешировать flat file list при открытии editor. НЕ перечитывать на каждое открытие Cmd+P. Invalidate по F5 или file watcher event.
Итерация 5: Performance-medium
-
File watcher -- opt-in: НЕ включать по умолчанию. Toggle "Watch for external changes". По умолчанию -- ручной refresh (F5). При включении:
fs.watch({ recursive: true })с фильтрацией (node_modules/.git/dist) и debounce 200ms. -
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 раз