* fix: add retry logic to sendInboxMessage for concurrent writes On Windows, parallel writes to the same inbox file cause race conditions where atomicWrite verification fails (another process overwrites between write and verify). Added retry loop (8 attempts) matching the existing pattern in addTaskComment. Bumps teamctl version to 11. Fixes CI failure: test (windows-latest) "parallel messages to same inbox" * fix: enhance CLI installer and session management - Updated the postinstall script in package.json to handle rebuild failures gracefully. - Added clearContext option in team launch requests to allow starting fresh sessions without resuming previous context. - Improved CLI installer logging by integrating raw output chunks for better terminal rendering. - Refactored components to utilize TerminalLogPanel for displaying installation logs, enhancing user experience during CLI installation. - Updated various services and hooks to support the new clearContext feature and raw logging. * fix: update MemberBadge and LaunchTeamDialog components for improved functionality - Modified MemberBadge to display 'lead' for team leads instead of the full name. - Refactored LaunchTeamDialog to simplify model selection logic and replace the Select component with a custom button-based interface for better user experience. - Enhanced KanbanTaskCard to include meta actions for task management, improving the layout and functionality for manual review tasks. * feat: auto-publish releases with stable download links - Change releaseType from draft to release for auto-publishing - Add upload-stable-links job to create version-agnostic asset copies - Update README with direct download URLs per platform - Add Requirements section to Installation - Remove downloads/platform badges - Add docs/RELEASE.md with versioning and release guide - Move community docs to .github/ * improvemtns * improvement * fix: handle Windows spawn EINVAL on non-ASCII paths and add helper utilities * improvements * fix: enhance child process environment handling for Windows - Added a helper function to build the child process environment with the correct HOME directory, addressing issues with non-ASCII usernames on Windows. - Updated CLI installer methods to utilize the new environment setup for improved compatibility and error handling. * refactor: replace execFile and spawn with execCli and spawnCli in CLI and TeamProvisioning services - Updated CliInstallerService and TeamProvisioningService to use execCli and spawnCli for improved error handling and compatibility, particularly on Windows. - Enhanced child process utility functions to better manage non-ASCII paths and provide consistent behavior across different platforms. - Adjusted tests to mock new child process utilities and verify correct usage in service methods. * fix * fix windows * feat: add download badges with direct links per platform * refactor: move download buttons to Installation section * refactor: move Docker files to docker/ and CHANGELOG to docs/ * refactor: move vite.standalone.config to docker/, remove .nvmrc * refactor: merge tsconfig.test.json into tsconfig.json * refactor: remove .editorconfig, .gitattributes, merge knip.json into package.json * fix: adjust macOS download badge sizing * 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. * feat: enhance in-app project editor with architecture documentation and service updates - Added detailed architecture and component hierarchy documentation for the in-app project editor. - Introduced `ProjectFileService` to manage file operations with improved path validation. - Updated `electron.vite.config.ts` to set `UV_THREADPOOL_SIZE` for better performance on Windows. - Deferred non-critical startup tasks in `index.ts` to avoid thread pool contention. - Enhanced `CliInstallerService` with timeout handling for status gathering to prevent UI hangs. - Added tests for `CliInstallerService` to ensure proper timeout behavior. * feat: enhance project editor with file management, Git integration, and UI improvements - Introduced `EditorFileWatcher` for live file change detection and `GitStatusService` for displaying Git status in the file tree. - Added context menu for file operations (create, delete) and implemented multi-tab support for the editor. - Enhanced user experience with keyboard shortcuts, search functionality, and breadcrumb navigation. - Updated IPC channels for file operations and integrated conflict detection during file saves. - Improved performance with file watcher optimizations and virtualized file tree rendering. * feat: enhance project editor with autosave, improved file management, and performance optimizations - Implemented draft autosave functionality to prevent data loss during crashes, with recovery options for unsaved changes. - Updated file management services to support better path validation and conflict detection. - Enhanced performance with optimized file watcher and caching strategies for project scanning. - Improved user experience with confirmation dialogs for unsaved changes and refined keyboard shortcuts. - Documented testing strategies and rollback plans for iterative development. * feat: integrate simple-git for enhanced Git status tracking and improve file watcher performance - Replaced direct Git command usage with `simple-git` for more reliable status tracking, including support for renamed files and conflict detection. - Updated IPC channels to reflect changes in Git status retrieval method. - Enhanced file watcher initialization on Windows to prevent UV thread pool saturation by starting watchers sequentially. - Improved application startup by staggering context system initialization and notification listeners to optimize performance. * feat: optimize IPC initialization and context management for improved app performance - Deferred IPC-heavy initialization to occur after the first paint to prevent app freezing on Windows. - Staggered notification listener setup to avoid saturating the UV thread pool during startup. - Updated context system initialization to be lazy, ensuring local context is always ready without upfront costs. - Enhanced data fetching sequence to reduce simultaneous IPC calls, improving overall responsiveness. * feat: enhance project editor with new error boundary and file handling improvements - Introduced `EditorErrorBoundary` component to catch runtime errors in CodeMirror, providing a fallback UI to prevent crashes. - Updated file handling logic to utilize `isbinaryfile` for more reliable binary detection, replacing manual null-byte scans. - Enhanced `openFile` method to prevent duplicate tabs for already opened files. - Improved documentation for new components and updated file lists to reflect recent changes. - Optimized file watcher and path validation processes for better performance and reliability. * feat: enhance TeamConfigReader with improved file handling and concurrency - Introduced `mapLimit` function to manage concurrent processing of team directories, optimizing performance. - Added `readFileHead` function to read the beginning of large configuration files efficiently. - Implemented `extractQuotedString` to safely extract values from JSON strings in configuration headers. - Enhanced error handling and validation for team configuration files, ensuring robust processing of team data. - Updated logic to handle large configuration files differently, improving overall reliability and performance. * feat: enhance team management with improved session and project path history handling - Introduced constants for maximum session and project path history limits to optimize memory usage. - Updated `TeamConfigReader` and `TeamProvisioningService` to limit session and project path history to defined maximums. - Enhanced `TeamMembersMetaStore` to handle large meta files more efficiently by checking file size before processing. - Refactored state management in `teamSlice` to include optimized lookups for team summaries by name and session ID. * feat: enhance GlobalTaskDetailDialog and TaskDetailDialog with loading state management - Added a loading state to the TaskDetailDialog to display a loading indicator while fetching team data. - Updated GlobalTaskDetailDialog to pass the loading state to TaskDetailDialog. - Modified the selectTeam function in teamSlice to accept options for skipping project auto-selection, improving team data handling. * feat: optimize team display name resolution and enhance file change handling - Introduced a caching mechanism for team display names to reduce redundant API calls and improve performance. - Updated file change event handling to debounce cache invalidation, preventing unnecessary rescans during rapid file changes. - Enhanced GlobalTaskDetailDialog and TaskDetailDialog to manage loading states and improve team data fetching logic. - Refactored teamSlice to streamline team selection and data loading processes. * fix: improve team selection logic and prevent duplicate fetches - Enhanced GlobalTaskDetailDialog to handle loading states more effectively, preventing unnecessary re-fetching of team data. - Updated selectTeam function in teamSlice to guard against duplicate in-flight fetches for the same team, improving performance and user experience. - Refactored dependencies in useEffect to ensure proper data loading behavior. * feat: enhance team data fetching with performance logging and timeout handling - Added performance logging to the handleGetData function, tracking the duration of team data retrieval and logging warnings for slow responses. - Implemented a timeout mechanism in the selectTeam function to prevent long-running fetch operations, improving user experience. - Enhanced the getTeamData method with detailed timing metrics for each data loading step, allowing for better performance analysis and debugging. * feat: implement timeout handling and logging for team data fetching - Added a timeout mechanism to the getBranch calls in TeamDataService to prevent hangs on Windows setups, improving reliability during team data retrieval. - Introduced performance logging in TeamDetailView and teamSlice to track the start and completion of team selection processes, enhancing debugging capabilities. - Updated error handling to provide clearer warnings during team provisioning and selection, improving user experience. * feat: enhance MarkdownViewer and task dialogs with loading state management and performance logging - Introduced character limits for Markdown content in MarkdownViewer to prevent UI freezes with large content. - Added state management for raw content display in MarkdownViewer, allowing users to expand and view large markdown files. - Implemented performance logging in GlobalTaskDetailDialog and TaskDetailDialog to track loading states and improve debugging. - Updated loading state handling in TaskDetailDialog to ensure accurate representation of loading conditions. * feat: enhance TaskCommentsSection with improved comment rendering and visibility management - Added state management for visible comments, allowing users to see a limited number of comments for better performance. - Implemented logic to cap the number of rendered comments, preventing UI freezes with large comment lists. - Introduced sorting for comments based on creation date to display the most recent comments first. - Updated the UI to inform users when only a subset of comments is being displayed, enhancing user experience. * feat: enhance MarkdownViewer with improved character limits and syntax highlighting management - Updated character limits for Markdown content to prevent UI freezes with large inputs. - Introduced logic to disable syntax highlighting for medium/large content and show raw previews for very large content. - Enhanced responsiveness of the MarkdownViewer by managing rendering based on content size. * refactor: remove console warnings from team-related components for cleaner logging - Eliminated console warnings in TeamDetailView, GlobalTaskDetailDialog, and TaskDetailDialog to streamline logging and reduce clutter during team data operations. - Updated the selectTeam function in teamSlice to remove unnecessary logging, enhancing performance and readability. * feat: add project editor with drag & drop file management - Backend: ProjectFileService with file CRUD, search, git status, file watcher - IPC: 12 editor channels with security validation and path containment - Store: editorSlice with multi-tab management, draft persistence, conflict detection - UI: CodeMirror 6 editor, file tree with DnD, search-in-files, context menus - Move: fs.rename with EXDEV fallback, full path remapping across all caches - Tests: comprehensive coverage for services, IPC handlers, store, and utilities * fix: rename closeTab/setActiveTab to closeEditorTab/setActiveEditorTab Resolve naming collision between editorSlice and tabSlice. Both slices defined closeTab and setActiveTab, and since editorSlice was spread last in the store composition, it silently overwrote the tabSlice methods, breaking tab management. * fix: editor improvements — isDir bug, scroll-to-line, Quick Open, a11y - Fix isDir heuristic: use backend-provided isDirectory instead of filename-based guessing (breaks for Makefile, .github, etc.) - Add scroll-to-line on search result click via editorPendingGoToLine - Add Cmd+Shift+W shortcut for toggling line wrap - Rewrite Quick Open to fetch all project files from backend API instead of flattening the loaded tree (limited to expanded dirs) - Fix fd leak in atomicWrite: close file handle in finally block - Add a11y: role=dialog/alert, aria-modal, aria-label on modals - Add type=button on error state buttons --------- Co-authored-by: Алексей <aleksei@example.com>
68 KiB
Архитектурный план: In-App Code Editor
Контекст
На странице TeamDetailView рядом с путём проекта (data.config.projectPath, строка ~761 файла TeamDetailView.tsx) добавляется кнопка, открывающая полноэкранный редактор кода прямо внутри приложения. Редактор базируется на CodeMirror 6 (уже используется в проекте -- 17 пакетов @codemirror/* в package.json), а не ProseMirror. Это решение основано на том, что CodeMirror -- единственный редактор кода в зависимостях проекта, с готовым набором языковых расширений и темой oneDark.
Оценки
- Надежность решения: 8/10 -- CodeMirror 6 проверен в продакшене (VS Code web, Obsidian), все зависимости уже в проекте.
- Уверенность в плане: 8/10 -- архитектура повторяет паттерны ChangeReviewDialog (full-screen overlay + file tree + CM editor).
Архитектурная диаграмма (ASCII)
┌─────────────────────────────────────────┐
│ TeamDetailView.tsx │
│ [FolderOpen icon] [Edit button] ◄──────┤ Кнопка запуска
└───────────────────┬─────────────────────┘
│ open={true}
┌───────────────────▼─────────────────────┐
│ CodeEditorOverlay (full-screen) │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ FileTreePanel│ │ EditorTabsPanel │ │
│ │ │ │ ┌────────────┐ │ │
│ │ ProjectTree │ │ │ EditorTab │ │ │
│ │ component │ │ │ EditorTab │ │ │
│ │ (recursive) │ │ └────────────┘ │ │
│ │ │ │ ┌────────────┐ │ │
│ │ │ │ │CodeMirror │ │ │
│ │ │ │ │EditorView │ │ │
│ └──────────────┘ │ └────────────┘ │ │
│ └──────────────────┘ │
└────────────────────────────────────────-┘
│ IPC
┌──────────────▼──────────────────────────┐
│ Preload Bridge │
│ editor.readDir / readFile / writeFile │
│ editor.createFile / deleteFile │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ Main Process: ProjectFileService │
│ (sandboxed path validation) │
│ ┌─────────────────────────────────┐ │
│ │ fs.readdir / fs.readFile / │ │
│ │ fs.writeFile / fs.unlink / │ │
│ │ fs.mkdir │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
1. Компонентная иерархия
1.1 Новые компоненты
Размещение: src/renderer/components/team/editor/
editor/
├── CodeEditorOverlay.tsx # Полноэкранный overlay (аналог ChangeReviewDialog)
├── FileTreePanel.tsx # Левая панель с деревом файлов
├── FileTreeNode.tsx # Рекурсивная нода дерева (файл / директория)
├── EditorTabsPanel.tsx # Правая панель: вкладки + CodeMirror
├── EditorTab.tsx # Одна вкладка открытого файла
├── CodeMirrorEditor.tsx # Обёртка CM6 для редактирования (не diff)
├── EditorToolbar.tsx # Панель инструментов (Save, Undo, Redo, язык)
├── EditorStatusBar.tsx # Status bar: Ln:Col, язык, отступы, кодировка (UX Review 17.1.4)
├── EditorEmptyState.tsx # Пустое состояние (нет открытых файлов + shortcuts шпаргалка)
├── EditorBinaryState.tsx # Заглушка для бинарных файлов (UX Review 17.1.6)
└── EditorErrorState.tsx # Заглушка для ошибок чтения файла (UX Review 17.2.5)
1.2 Принцип Single Responsibility
| Компонент | Ответственность |
|---|---|
CodeEditorOverlay |
Layout: fixed inset-0, z-50, header/close, split layout |
FileTreePanel |
Загрузка дерева, expand/collapse, поиск, контекстное меню |
FileTreeNode |
Рендер одной ноды, иконка, клик, drag |
EditorTabsPanel |
Управление открытыми табами, переключение |
CodeMirrorEditor |
CM6 lifecycle: create/destroy EditorView, extensions, keybindings |
EditorToolbar |
Действия: Save (Cmd+S), язык, отступы, кодировка |
1.3 Паттерн overlay (повтор ChangeReviewDialog)
Вместо <Dialog> от Radix используем raw <div className="fixed inset-0 z-50"> -- точная копия паттерна из ChangeReviewDialog.tsx (строка 508). Причины:
- Radix Dialog ограничивает фокус внутри портала, что конфликтует с CM6
- Full-screen overlay не нуждается в backdrop/animation -- просто замена контента
- macOS traffic light padding:
var(--macos-traffic-light-padding-left, 72px)в header
2. State Management
2.1 Zustand slice: editorSlice.ts
Решение: Новый slice в src/renderer/store/slices/editorSlice.ts.
Обоснование: Состояние редактора (открытые табы, unsaved changes, active tab) должно переживать перемонтирование компонента overlay (например, если юзер случайно закрыл и открыл снова -- unsaved файлы должны быть на месте).
EditorSlice {
// --- Данные ---
editorProjectPath: string | null // Путь открытого проекта
editorFileTree: FileTreeNode | null // Корневое дерево
editorFileTreeLoading: boolean
editorFileTreeError: string | null
editorOpenTabs: EditorFileTab[] // Открытые вкладки
editorActiveTabId: string | null // Активная вкладка
editorFileContents: Record<string, string> // filePath → content (read-only cache)
editorFileContentsLoading: Record<string, boolean>
// ПЕРЕСМОТРЕНО (Performance Review 19.4): НЕ хранить modified content здесь!
// Контент живёт в EditorState (Map<tabId, EditorState> в useRef).
// Вместо Record<string, string> использовать Set<string> для dirty flags:
editorModifiedFiles: Set<string> // filePath set — dirty markers only
editorSaving: Record<string, boolean> // filePath → saving in progress
editorSaveError: Record<string, string> // filePath → save error
// --- Действия ---
openEditor: (projectPath: string) => Promise<void>
closeEditor: () => void
loadFileTree: (dirPath: string) => Promise<void>
expandDirectory: (dirPath: string) => Promise<void>
openFile: (filePath: string) => Promise<void>
closeTab: (tabId: string) => void
setActiveTab: (tabId: string) => void
updateContent: (filePath: string, content: string) => void
saveFile: (filePath: string) => Promise<void>
saveAllFiles: () => Promise<void>
discardChanges: (filePath: string) => void
createFile: (parentDir: string, name: string) => Promise<void>
deleteFile: (filePath: string) => Promise<void>
createDirectory: (parentDir: string, name: string) => Promise<void>
}
2.2 Локальное состояние компонентов
Не выносить в store (а хранить в useState):
- Scroll position дерева файлов
- CM6 EditorView ref
- Размер панелей (resizable split)
- Поисковый запрос в дереве файлов
- Состояние контекстного меню
2.3 Модель EditorFileTab
interface EditorFileTab {
id: string // = filePath (уникальный ключ)
filePath: string // Абсолютный путь
fileName: string // Имя файла для отображения
language: string // Определяется по расширению
isModified: boolean // Есть unsaved changes (derived)
}
2.4 Интеграция в AppState
Файл src/renderer/store/types.ts -- добавить EditorSlice в union type AppState.
3. IPC API Design
3.1 Новые IPC-каналы
Файл: src/preload/constants/ipcChannels.ts
// =============================================================================
// Editor API Channels
// =============================================================================
EDITOR_READ_DIR = 'editor:readDir'
EDITOR_READ_FILE = 'editor:readFile'
EDITOR_WRITE_FILE = 'editor:writeFile'
EDITOR_CREATE_FILE = 'editor:createFile'
EDITOR_DELETE_FILE = 'editor:deleteFile'
EDITOR_CREATE_DIR = 'editor:createDir'
EDITOR_RENAME = 'editor:rename'
EDITOR_FILE_EXISTS = 'editor:fileExists'
3.2 IPC-типы
Файл: src/shared/types/editor.ts (NEW)
FileTreeEntry {
name: string
path: string // Абсолютный путь
type: 'file' | 'directory'
size?: number // Только для файлов
children?: FileTreeEntry[] // Только для директорий (lazy)
}
ReadDirResult {
entries: FileTreeEntry[]
truncated: boolean // Если > MAX_DIR_ENTRIES
}
ReadFileResult {
content: string
size: number
truncated: boolean // Если > MAX_FILE_SIZE
encoding: string
}
3.3 Паттерн IPC handler
Файл: src/main/ipc/editor.ts (NEW)
Повторяет паттерн review.ts:
- module-level state (
let fileService: ProjectFileService | null) initializeEditorHandlers(service)registerEditorHandlers(ipcMain)removeEditorHandlers(ipcMain)wrapHandlerизsrc/main/ipc/ipcWrapper.ts(общий, НЕ копия изreview.ts)
3.4 ElectronAPI расширение
Файл: src/shared/types/api.ts -- добавить EditorAPI interface и свойство editor: EditorAPI в ElectronAPI.
Файл: src/preload/index.ts -- добавить секцию editor: { ... } в объект electronAPI, все через invokeIpcWithResult<T>().
4. Main Process: ProjectFileService
4.1 Сервис
Файл: src/main/services/editor/ProjectFileService.ts (NEW)
Единственная ответственность: безопасные файловые операции внутри заданного projectPath.
РЕВИЗИЯ: Сервис stateless (без
rootPathв конструкторе). Каждый метод принимаетprojectRootкак первый аргумент. Паттерн аналогиченTeamDataService— не привязан к одному проекту.
Критическая безопасность: Path traversal prevention через validateFilePath() из pathValidation.ts.
ProjectFileService {
// Stateless — нет конструктора с rootPath
// Все методы принимают projectRoot + проверяют через validateFilePath()
readDir(projectRoot: string, dirPath: string, depth?: number): Promise<ReadDirResult>
readFile(projectRoot: string, filePath: string): Promise<ReadFileResult>
writeFile(projectRoot: string, filePath: string, content: string): Promise<void>
createFile(projectRoot: string, parentDir: string, name: string, content?: string): Promise<void>
deleteFile(projectRoot: string, filePath: string): Promise<void>
createDir(projectRoot: string, parentDir: string, name: string): Promise<void>
rename(projectRoot: string, oldPath: string, newPath: string): Promise<void>
fileExists(projectRoot: string, filePath: string): Promise<boolean>
}
4.2 Path Validation
КРИТИЧЕСКИ ВАЖНО: Использовать validateFilePath() из src/main/utils/pathValidation.ts, а НЕ писать свой assertInsideRoot. Существующая функция уже обрабатывает:
- Нормализацию пути через
path.resolve() - Symlink resolution через
fs.realpathSync.native() - Проверку sensitive patterns (
.env,.ssh, credentials и т.д.) - Проверку что realpath тоже внутри allowed directories
- Cross-platform поддержку (Windows case-insensitive)
import { validateFilePath } from '@main/utils/pathValidation';
function assertInsideProject(absolutePath: string, projectRoot: string): string {
const result = validateFilePath(absolutePath, projectRoot);
if (!result.valid) {
throw new Error(`Access denied: ${result.error}`);
}
return result.normalizedPath!;
}
Дополнительные проверки для editor (сверх validateFilePath):
- Symlink-проверка для readDir: при рекурсивном обходе каждый entry может быть symlink. Нужно
fs.lstat()+fs.realpath()для каждого entry, проверяя что target внутри projectRoot. - Валидация имён файлов при создании: запрет NUL bytes, запрет
./..как имени, запрет/и\в имени, максимальная длина 255 символов. - TOCTOU mitigation: использовать
O_NOFOLLOWпри открытии файлов или проверять послеopen()черезfstat(), что дескриптор указывает на файл внутри projectRoot. - Запрет записи в .git/: добавить
.gitв список запрещённых для записи директорий (чтение можно разрешить для отображения, но НЕ запись).
4.3 Файловые лимиты и защита от DoS
MAX_FILE_SIZE = 2 * 1024 * 1024 // 2 MB -- безопасный лимит для IPC + CM6
MAX_WRITE_SIZE = 2 * 1024 * 1024 // 2 MB -- лимит на запись (защита от memory bomb)
MAX_DIR_ENTRIES = 5_000 // Защита от node_modules-подобных директорий
MAX_DIR_DEPTH = 15 // Максимальная глубина рекурсии
MAX_FILENAME_LENGTH = 255 // POSIX лимит
MAX_PATH_LENGTH = 4096 // PATH_MAX
IGNORED_DIRS = ['.git', 'node_modules', '.next', 'dist', '__pycache__', '.cache', '.venv', '.tox', 'vendor']
IGNORED_FILES = ['.DS_Store', 'Thumbs.db']
// Защита от чтения device файлов и спецфайлов
BLOCKED_PATHS = ['/dev/', '/proc/', '/sys/', '\\\\.\\'] // device files на Linux/macOS/Windows
Важно: Перед чтением файла обязательно проверить через fs.lstat():
stats.isFile()=== true (не directory, не device, не socket, не FIFO)stats.size<= MAX_FILE_SIZE (не читать файл если stat показывает огромный размер)- НЕ использовать
stats.isSymbolicLink()для решения -- вместо этогоfs.realpath()+ повторная проверка containment
Перед записью: проверить Buffer.byteLength(content, 'utf8') <= MAX_WRITE_SIZE до вызова fs.writeFile().
4.4 Регистрация в handlers.ts
Файл: src/main/ipc/handlers.ts
- Импорт
initializeEditorHandlers,registerEditorHandlers,removeEditorHandlers - Создание
ProjectFileService(stateless, без аргументов) вinitializeIpcHandlers - Регистрация при инициализации
5. Дерево файлов
5.1 Рекурсивная модель с lazy-loading
Дерево НЕ грузится целиком. Начальная загрузка -- только root level (depth=1). При expand директории -- IPC editor:readDir для конкретной папки.
FileTreeNode (renderer-side) {
name: string
path: string
type: 'file' | 'directory'
size?: number
// Для директорий:
children: FileTreeNode[] | null // null = не загружены
expanded: boolean
loading: boolean
}
5.2 Хранение состояния дерева
expandedDirs: Set<string> -- хранить в editorSlice. При re-open editor -- дерево подгружается заново, но expanded-состояние сохраняется.
5.3 Фильтрация и сортировка
- Скрывать:
.git,node_modules,dist,__pycache__(configurable) - Сортировка: директории сначала, затем файлы; внутри группы -- alphabetical
- Поиск: fuzzy filter по имени файла (локальный, без IPC)
5.4 Контекстное меню
Правый клик на ноде:
- Файл: Open, Delete, Rename, Copy Path
- Директория: New File, New Directory, Delete, Rename, Copy Path
- Пустое место: New File, New Directory
6. CodeMirror интеграция
6.1 Подход
Компонент CodeMirrorEditor.tsx -- обёртка аналогичная MembersJsonEditor.tsx (строки 27-59) и CodeMirrorDiffView.tsx, но для single-file editing (не diff).
6.2 Extensions (переиспользование)
Из уже имеющихся в CodeMirrorDiffView.tsx:
- Language detection (файл → extension → LanguageDescription)
Все 17 языков уже подключены: JS/TS, Python, Rust, Go, Java, C++, CSS, HTML,
JSON, YAML, XML, SQL, PHP, Markdown, Less, Sass
- oneDarkHighlightStyle (уже импортируется)
- lineNumbers()
- history() + historyKeymap
- indentWithTab
- defaultKeymap
- syntaxHighlighting()
Дополнительно для editor (не diff):
- closeBrackets + closeBracketsKeymap (уже используется в MembersJsonEditor)
- bracketMatching (уже используется в MembersJsonEditor)
- EditorView.updateListener для onChange
- Cmd+S keymap для save
- search + searchKeymap (Cmd+F)
- indentUnit настройка (2/4 spaces)
- EditorView.lineWrapping (toggle)
- highlightActiveLine
- highlightActiveLineGutter
6.3 Определение языка по расширению
Функция getLanguageExtension(fileName) -- уже реализована в CodeMirrorDiffView.tsx (примерно строки 1-25, маппинг extension -> language plugin). Вынести в общий util src/renderer/utils/codemirrorLanguage.ts для переиспользования.
6.4 Тема
Единая тема для всего приложения: oneDark + CSS custom properties из index.css. Дополнительная кастомизация через EditorView.theme({}):
- Фон:
var(--color-surface) - Шрифт:
ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace - font-size: 13px (чуть крупнее чем в diff view)
6.5 Управление EditorView lifecycle
ПЕРЕСМОТРЕНО после Performance Review (секция 19.1-19.2). Оригинальный подход CSS show/hide заменён на EditorState pooling.
Один-> ОдинEditorViewна открытый tabEditorViewна ВЕСЬ редактор (активный файл)- При закрытии tab:
savedStates.delete(tabId) - Хранить
Map<tabId, EditorState>в ref (НЕ EditorView!) - При переключении tab:
savedStates.set(oldId, view.state)->view.destroy()->new EditorView({ state: savedStates.get(newId), parent: container }) - LRU eviction при >30 states: сохранить content + cursor, вытеснить undo history
- Паттерн: аналог initialState в CodeMirrorDiffView.tsx (строки 699-705)
7. Tab-система для нескольких файлов
7.1 Модель
openTabs: EditorFileTab[]
activeTabId: string | null
7.2 Поведение
- Клик на файл в дереве:
- Если tab уже открыт -- activate
- Если нет -- создать tab, загрузить содержимое через IPC, activate
- Закрытие tab:
- Если есть unsaved changes -- confirm dialog (Save / Discard / Cancel)
- Cmd+W закрывает активный tab
- Modified indicator: точка на tab (аналог VS Code)
- Порядок табов: по порядку открытия, drag-to-reorder не нужен на первой итерации
8. Error Handling Strategy
8.1 Уровни ошибок
| Уровень | Обработка | Пример |
|---|---|---|
| IPC failure | Toast/banner в overlay | Сеть, main process crash |
| File read error | Inline в tab | ENOENT, EACCES, binary file |
| File write error | Inline + retry | EACCES, disk full |
| Path traversal | Reject + log | Попытка выйти за projectPath |
| File too large | Inline warning | > MAX_FILE_SIZE |
8.2 Паттерн ошибок в slice
Повторяет teamSlice:
editorFileTreeError: string | null
editorSaveError: Record<string, string> // per-file
8.3 Main process
wrapEditorHandler<T>() -- ловит все исключения, возвращает IpcResult<T>.
8.4 Renderer
unwrapIpc('editor:readFile', ...) -- стандартный паттерн из unwrapIpc.ts.
9. Производительность
9.1 Большие директории
- Lazy loading: грузим дерево по одному уровню, expand подгружает children
- Фильтрация:
node_modules,.gitи т.д. фильтруются на стороне main process (НЕ отправляются по IPC) - Лимит: MAX_DIR_ENTRIES = 10,000 entries per directory, truncation flag
9.2 Большие файлы
- Лимит: MAX_FILE_SIZE = 5MB. Больше -- показываем warning, предлагаем открыть в внешнем редакторе (
shell:openPath) - Бинарные файлы: Определять по magic bytes / extension. Показывать "Binary file, cannot edit"
- CM6 производительность: CodeMirror 6 обрабатывает файлы до 5MB без проблем (virtual rendering)
9.3 Оптимизация IPC
- File content caching: кэшируем
editorFileContentsв store. Invalidate при save. - Debounced onChange: updateContent вызывается при каждом keystroke, но это локальная операция (set state). Фактический save только по Cmd+S.
- Tree caching: после загрузки дерево хранится в store. Re-fetch только при explicit refresh (F5 или кнопка refresh).
9.4 Memory
- Удалять CM EditorView при закрытии tab для освобождения памяти
- Не хранить больше 20 одновременно открытых EditorView (soft limit, предупреждение)
10. Data Flow
10.1 Открытие редактора
1. Юзер кликает кнопку [Code] рядом с projectPath в TeamDetailView
2. editorSlice.openEditor(data.config.projectPath)
3. set({ editorProjectPath, editorFileTreeLoading: true })
4. IPC: editor:readDir(projectPath, depth=1)
5. Main: ProjectFileService.readDir() → валидация пути → fs.readdir
6. Результат: FileTreeEntry[]
7. set({ editorFileTree, editorFileTreeLoading: false })
8. CodeEditorOverlay рендерится (fixed inset-0 z-50)
10.2 Открытие файла
1. Юзер кликает на файл в FileTreePanel
2. editorSlice.openFile(filePath)
3. Проверка: есть ли уже tab с этим filePath?
ДА → setActiveTab(tabId)
НЕТ → создать tab, IPC: editor:readFile(filePath)
4. Main: ProjectFileService.readFile() → валидация → fs.readFile
5. Результат: ReadFileResult { content, size, truncated }
6. set({ editorFileContents[filePath]: content })
7. CM EditorState создаётся, единственный EditorView пересоздаётся
10.3 Сохранение файла
1. Юзер нажимает Cmd+S (или кнопку Save)
2. editorSlice.saveFile(filePath)
3. content = EditorState (из useRef Map) ?? editorFileContents[filePath]
4. set({ editorSaving[filePath]: true })
5. IPC: editor:writeFile(filePath, content)
6. Main: ProjectFileService.writeFile() → валидация → fs.writeFile (atomic via tmp+rename)
7. set({ editorSaving: false, editorModifiedFiles: remove filePath })
8. Tab isModified indicator исчезает
10.4 Создание/удаление файла
Создание:
1. Юзер через контекстное меню → "New File"
2. Inline input в дереве (имя файла)
3. IPC: editor:createFile(parentDir, name)
4. Main: fs.writeFile(path.join(parentDir, name), '')
5. Обновить дерево: expandDirectory(parentDir)
6. Автоматически открыть новый файл в tab
Удаление:
1. Контекстное меню → "Delete"
2. Confirm dialog
3. IPC: editor:deleteFile(filePath)
4. Main: fs.unlink (файл) или fs.rm (директория, recursive)
5. Закрыть tab если был открыт
6. Обновить дерево
11. Keyboard Shortcuts
| Shortcut | Действие |
|---|---|
Cmd+S |
Сохранить активный файл |
Cmd+Shift+S |
Сохранить все |
Cmd+W |
Закрыть активный tab |
Cmd+P |
Quick Open (поиск файла) -- Phase 2 |
Cmd+F |
Поиск в файле (CM6 search) |
Escape |
Закрыть overlay (с confirm при unsaved changes) |
Cmd+Shift+[ / Cmd+Shift+] |
Переключение табов влево/вправо |
Ctrl+Tab / Ctrl+Shift+Tab |
Переключение табов (MRU) |
Cmd+B |
Toggle file tree sidebar |
Cmd+G |
Go to line (CM6 gotoLine) |
Cmd+Z / Cmd+Shift+Z |
Undo/Redo (CM6 native) |
12. Новые зависимости
Нет новых npm-зависимостей! Все нужные пакеты уже в package.json:
- CodeMirror 6 -- 17 пакетов
@codemirror/* - lucide-react -- иконки (File, Folder, FolderOpen, Save, X, Plus, Trash2)
- Radix UI -- для контекстного меню (Popover) и confirm dialog (Dialog)
13. План итераций реализации
Итерация 1: Read-Only File Browser
FileEditorServiceсreadDir+readFile(main process)- IPC каналы
editor:readDir,editor:readFile editorSlice(минимальный: tree + openFile + tabs)CodeEditorOverlay+FileTreePanel+CodeMirrorEditor(read-only)- Кнопка в TeamDetailView
Итерация 2: File Editing + Save
writeFileв сервисе + IPC- Modified content tracking в store
- Cmd+S save
- Unsaved changes indicator (dot on tab)
- Close tab с confirm
Итерация 3: File Operations
createFile,deleteFile,createDir,renameв сервисе- Контекстное меню в дереве файлов
- Inline rename в дереве
Итерация 4: Polish
- Quick Open (Cmd+P) -- fuzzy search по файлам
- Binary file detection
- Large file warning
- File watcher integration (auto-refresh tree при внешних изменениях)
- Resizable split panels
14. Список файлов для создания/модификации
Новые файлы (~15)
| Файл | Описание |
|---|---|
src/shared/types/editor.ts |
Типы: FileTreeEntry, ReadDirResult, ReadFileResult |
src/main/services/editor/ProjectFileService.ts |
Main process сервис файловых операций (stateless) |
src/main/ipc/editor.ts |
IPC handlers для editor |
src/main/ipc/ipcWrapper.ts |
Общий createIpcWrapper() (извлечь из review.ts) |
src/renderer/store/slices/editorSlice.ts |
Zustand slice (итерация 2+) |
src/renderer/components/team/editor/ProjectEditorOverlay.tsx |
Full-screen overlay |
src/renderer/components/team/editor/EditorFileTree.tsx |
Обёртка над generic FileTree |
src/renderer/components/common/FileTree.tsx |
Generic FileTree с render-props (рефакторинг из ReviewFileTree) |
src/renderer/components/team/editor/EditorTabsPanel.tsx |
Табы + editor |
src/renderer/components/team/editor/CodeMirrorEditor.tsx |
CM6 wrapper |
src/renderer/components/team/editor/EditorToolbar.tsx |
Toolbar |
src/renderer/components/team/editor/EditorEmptyState.tsx |
Empty state |
src/renderer/utils/codemirrorLanguages.ts |
Языковой маппинг (извлечь из CodeMirrorDiffView) |
src/renderer/utils/codemirrorTheme.ts |
Базовая тема CM (извлечь из diffTheme) |
src/renderer/utils/fileTreeBuilder.ts |
buildTree + сортировка (извлечь из ReviewFileTree) |
Модификации (~10)
| Файл | Изменение |
|---|---|
src/preload/constants/ipcChannels.ts |
+8 констант EDITOR_* |
src/preload/index.ts |
+секция editor: { ... } в electronAPI |
src/shared/types/api.ts |
+EditorAPI interface, +editor: EditorAPI в ElectronAPI |
src/shared/types/index.ts |
+export из editor.ts |
src/main/ipc/handlers.ts |
+регистрация editor handlers |
src/main/ipc/review.ts |
Заменить локальный wrapReviewHandler на import из ipcWrapper.ts |
src/renderer/store/types.ts |
+EditorSlice в AppState union (итерация 2) |
src/renderer/store/index.ts |
+createEditorSlice (итерация 2) |
src/renderer/components/team/TeamDetailView.tsx |
+кнопка Code + импорт ProjectEditorOverlay |
src/renderer/components/team/review/ReviewFileTree.tsx |
Рефакторинг: использовать generic FileTree + fileTreeBuilder |
src/renderer/components/team/review/CodeMirrorDiffView.tsx |
Рефакторинг: импортировать из codemirrorLanguages.ts и codemirrorTheme.ts |
15. Риски и митигации
| Риск | Вероятность | Митигация |
|---|---|---|
| Path traversal через IPC | Средняя | validateFilePath() из pathValidation.ts на КАЖДОМ IPC handler |
| CM6 тормозит на файлах >2MB | Низкая | Hard limit 2MB + warning + external editor fallback |
| node_modules в дереве -- OOM | Высокая | IGNORED_DIRS фильтр на main process + MAX_DIR_ENTRIES |
| Race condition при save (TOCTOU) | Высокая | Atomic write (tmp + rename) + fstat после open + saving flag |
| Unsaved data loss при crash | Средняя | Phase 2: autosave в localStorage/IndexedDB |
| Symlink escape из rootPath | Высокая | validateFilePath() уже делает fs.realpathSync.native() + re-check |
| Device file DoS (/dev/zero) | Средняя | fs.lstat() + isFile() проверка ДО чтения |
| Credential leakage (.env, .key) | Высокая | validateFilePath() проверяет SENSITIVE_PATTERNS |
| XSS через имена файлов | Низкая | React экранирует автоматически; НЕ использовать innerHTML |
| IPC flooding | Средняя | Debounce на renderer + AbortController |
| ReDoS в searchInFiles | Средняя | Только literal search, НЕ regex от пользователя |
16. Архитектурная ревизия (SOLID / DRY / Clean Architecture)
Добавлено после ревизии архитектором. Все замечания основаны на анализе реального кода проекта.
16.1 SOLID-анализ
S -- Single Responsibility
Проблема 1: FileTreePanel.tsx несёт двойную ответственность.
В плане FileTreePanel отвечает и за загрузку данных дерева (IPC вызовы, expand/collapse), и за рендеринг UI (поиск, контекстное меню).
Решение: Разделить на два слоя:
FileTreePanel.tsx-- чистый UI: рендерит дерево, принимает данные через store- Логика загрузки и expand -- ТОЛЬКО в
editorSlice.ts(actionsloadFileTree,expandDirectory) - Контекстное меню -- отдельный
EditorContextMenu.tsx(уже запланирован на итерацию 3)
Проблема 2: CodeMirrorEditor.tsx смешивает CM lifecycle + keybindings + onChange.
Решение: Извлечь extensions builder в отдельный buildEditorExtensions.ts (аналогично buildExtensions() в CodeMirrorDiffView.tsx строки 477-688). Keybindings (Cmd+S и др.) -- часть extensions, но собираются в builder, а не в компоненте.
O -- Open/Closed
Проблема: FileTree не расширяем через render-prop.
ReviewFileTree.tsx уже содержит TreeItem с review-специфичным рендерингом (FileStatusIcon, +/- lines). EditorFileTree будет содержать свой рендеринг (dirty marker, file type icon). Два дерева -- два набора рендеринга без общей абстракции.
Решение (render-prop / compound components):
// Общий generic FileTree
interface FileTreeProps<T extends { name: string; fullPath: string; isFile: boolean }> {
nodes: T[];
activeNodePath: string | null;
onNodeClick: (node: T) => void;
renderNodeExtra?: (node: T) => React.ReactNode; // Правая часть (статус/кол-во строк)
renderNodeIcon?: (node: T) => React.ReactNode; // Иконка слева от имени
collapsedFolders: Set<string>;
onToggleFolder: (fullPath: string) => void;
}
ReviewFileTree добавляет FileStatusIcon + +/- строки через renderNodeExtra.
EditorFileTree добавляет dirty-маркер и file type icon.
Оба дерева используют один buildTree() и TreeItem рендеринг.
L -- Liskov Substitution
Наследований в плане нет (React -- composition over inheritance). Корректно.
FileTreeNode должен расширять FileTreeEntry, а не дублировать поля:
// shared/types/editor.ts
interface FileTreeEntry { name: string; path: string; type: 'file' | 'directory'; size?: number; }
// renderer (local type)
interface FileTreeNode extends FileTreeEntry {
children: FileTreeNode[] | null;
expanded: boolean;
loading: boolean;
}
I -- Interface Segregation
Проблема: editorSlice с 15+ actions -- слишком толстый интерфейс.
Сравнение: changeReviewSlice содержит ~25 actions и это одна из самых сложных фич в проекте.
Решение: Логически разделить EditorSlice на 4 группы (оставить в одном файле, т.к. Zustand slices -- flat intersection, но документировать секциями):
// Группа 1: File tree state + actions
editorProjectPath, editorFileTree, editorFileTreeLoading, editorFileTreeError
openEditor, closeEditor, loadFileTree, expandDirectory
// Группа 2: Tab management
editorOpenTabs, editorActiveTabId
openFile, closeTab, setActiveTab
// Группа 3: Content + Save
editorFileContents, editorModifiedContents, editorSaving, editorSaveError
updateContent, saveFile, saveAllFiles, discardChanges
// Группа 4: File operations (итерация 3)
createFile, deleteFile, createDirectory
D -- Dependency Inversion
Проблема: CodeMirrorEditor.tsx напрямую зависит от конкретных CM extensions.
Решение: Extensions собираются в фабрике buildEditorExtensions(options):
interface EditorExtensionOptions {
readOnly: boolean;
fileName: string;
onContentChanged?: (content: string) => void;
onSave?: () => void;
tabSize?: number;
lineWrapping?: boolean;
}
Компонент вызывает buildEditorExtensions(opts) и не знает о конкретных extensions.
16.2 DRY-анализ
Проблема 1: Дублирование buildTree() + сортировки.
ReviewFileTree.tsx строки 42-83 содержат buildTree() с collapse-логикой. EditorFileTree будет реализовывать аналогичную, но с другим источником данных.
Решение (обязательное):
- Извлечь generic
buildTree<T>(items, getPath, isFile)вsrc/renderer/utils/fileTreeBuilder.ts - Сортировка (dirs first, alphabetical) тоже в
fileTreeBuilder.ts ReviewFileTree+EditorFileTreeиспользуют одну и ту же функцию
Проблема 2: Тема CodeMirror -- частичное дублирование с diffTheme.
~50% стилей diffTheme (&, .cm-gutters, .cm-scroller, .cm-content, .cm-cursor, .cm-selectionBackground) идентичны.
Решение:
// src/renderer/utils/codemirrorTheme.ts
export const baseEditorTheme = EditorView.theme({/* общие стили */});
// CodeMirrorDiffView.tsx -- импортирует baseEditorTheme + свои diff-стили
// CodeMirrorEditor.tsx -- импортирует baseEditorTheme + свои editor-стили
Проблема 3: wrapEditorHandler -- копия wrapReviewHandler.
В плане wrapEditorHandler<T>() в editor.ts -- 1:1 копия из review.ts (строки 133-145).
Решение: Извлечь общий createIpcWrapper(logPrefix) в src/main/ipc/ipcWrapper.ts:
export function createIpcWrapper(logPrefix: string) {
const log = createLogger(logPrefix);
return async function wrap<T>(op: string, fn: () => Promise<T>): Promise<IpcResult<T>> {
try { return { success: true, data: await fn() }; }
catch (error) {
const msg = error instanceof Error ? error.message : String(error);
log.error(`handler error [${op}]:`, msg);
return { success: false, error: msg };
}
};
}
16.3 Clean Architecture -- направление зависимостей
Потоки зависимостей проверены -- корректны:
shared/types/editor.ts (чистые типы, zero deps)
<- main/services/editor/ (зависит от fs, path, shared/types)
<- main/ipc/editor.ts (зависит от service + shared types)
<- preload/index.ts (зависит от ipcChannels)
<- renderer/store/ (зависит от api layer + shared types)
<- renderer/components/ (зависит от store + utils)
Проблема: FileEditorService принимает rootPath в конструкторе.
Привязывает один сервис к одному проекту. При переключении команды -- нужно пересоздавать.
Решение: Stateless service (рекомендуется, 9/10).
Каждый метод принимает projectRoot как аргумент. Валидация -- в каждом методе.
Это паттерн TeamDataService (нет привязки к конкретной команде в конструкторе).
В handlers.ts создаётся один экземпляр ProjectFileService() без аргументов.
16.4 Security -- переиспользование существующей валидации
Проблема: План описывает свой assertInsideRoot(), но в проекте уже есть validateFilePath() в src/main/utils/pathValidation.ts которая:
- Проверяет абсолютность пути
- Предотвращает path traversal
- Блокирует sensitive files (.ssh, .env, .pem и т.д.)
- Проверяет symlink escapes через
fs.realpathSync
Решение: НЕ писать свой assertInsideRoot(). Использовать validateFilePath(filePath, projectRoot) из pathValidation.ts. Дополнительно нужна ТОЛЬКО проверка что projectRoot -- валидный абсолютный путь (однократно при openEditor).
16.5 Именование -- приведение к единому стилю
В plan-architecture.md сервис назван FileEditorService, в plan-iterations.md -- ProjectFileService.
Рекомендация: Использовать ProjectFileService везде -- лучше отражает суть (файловые операции в рамках проекта), не путается с "editor" (который в renderer).
17. UX Review
Добавлено после UX-ревью. Анализ user journeys, keyboard-first, accessibility, edge cases.
17.1 Критично для MVP
17.1.1 Unsaved changes при закрытии overlay (Escape / кнопка X)
Проблема: В секции 11 Escape закрывает overlay, но нигде не описано, что происходит с unsaved changes при закрытии ВСЕГО overlay. В секции 7.2 confirm описан только для закрытия отдельного tab, не для overlay.
Рекомендация: При Escape или клике на X, если есть ЛЮБОЙ таб с isModified: true:
- Показать
confirm()(существующийConfirmDialog): "You have unsaved changes in N files." - Три кнопки: Save All & Close, Discard & Close, Cancel
Escapeвнутри confirm = Cancel (возврат к редактору)
Добавить в editorSlice:
hasUnsavedChanges: () => boolean // derived: Object.keys(editorModifiedContents).length > 0
17.1.2 Файл удалён извне пока открыт в табе
Проблема: Нигде не описано, что делать если файл, открытый в табе, удалён или переименован на диске (другим процессом, CLI-агентом). Claude Agent активно меняет файлы -- это реальный сценарий.
Рекомендация:
- При попытке
saveFileс ENOENT -- показать inline-ошибку в табе: "File was deleted. Create new? / Close tab" - При
editor:change(FileWatcher, итерация 5) -- если файл удалён, показать subtle banner: "File no longer exists on disk" - Для MVP (без FileWatcher): проверять
fileExistsпередwriteFile. Если ENOENT -- показать ошибку, не падать.
17.1.3 Два таба с одинаковым именем (разные пути)
Проблема: EditorFileTab.fileName -- просто имя файла. Если открыть src/main/utils/index.ts и src/renderer/utils/index.ts -- оба таба покажут "index.ts". Различить невозможно.
Рекомендация: VS Code решает добавлением минимального disambiguating parent:
index.ts (main/utils) index.ts (renderer/utils)
Утилита getDisambiguatedTabLabel(tabs) в src/renderer/utils/tabLabelDisambiguation.ts.
17.1.4 Status bar (line:col, язык, кодировка)
Проблема: В плане нет status bar -- базовый элемент любого код-редактора.
Рекомендация: EditorStatusBar.tsx -- нижняя полоска overlay:
[Ln 42, Col 15] | [TypeScript] | [UTF-8] | [Spaces: 2] | [LF]
CSS: bg-surface-sidebar border-t border-border text-text-muted text-xs h-6
17.1.5 Keyboard shortcuts -- конфликт Cmd+[/]
Проблема: Секция 11: Cmd+[ / Cmd+] для табов. Но в VS Code и CM6 это indent/outdent.
Рекомендация: Cmd+Shift+[ / Cmd+Shift+] для табов. Ctrl+Tab/Ctrl+Shift+Tab как альтернатива.
17.1.6 Binary файлы -- конкретный UI
Проблема: Секция 9.2 -- только текст, нет дизайна.
Рекомендация: EditorBinaryState.tsx вместо CM6: иконка, тип/размер, кнопки "Open in System Viewer" и "Close Tab". Добавить isBinary: boolean в ReadFileResult.
17.1.7 Accessibility: ARIA roles
Проблема: ReviewFileTree -- только aria-label. Нет role="tree", role="treeitem", aria-expanded.
Рекомендация:
- File tree:
role="tree",role="treeitem",aria-expanded,role="group", arrow keys - Tab bar:
role="tablist",role="tab",role="tabpanel",aria-selected
17.1.8 Focus management
Проблема: Не описано, куда идёт фокус при открытии/закрытии overlay.
Рекомендация:
- Открытие: фокус на первый файл в дереве (или CM6 если таб открыт)
- Закрытие: вернуть фокус на кнопку "Open in Editor" (
returnFocusRef) inertатрибут на фон пока overlay открыт
17.2 Важно, но не блокирует MVP
17.2.1 Discoverability -- подсказки горячих клавиш
EditorEmptyStateпоказывает шпаргалку shortcuts- Tooltip на кнопках toolbar: "Save (Cmd+S)"
- Кнопка
?в header -- модальное окно со всеми шорткатами
17.2.2 Пустой проект и проект с 1 файлом
- 0 файлов: "No files found. Create a new file?" + кнопка
- 1 файл: автоматически открыть в табе
- Все скрыты: "All files are in excluded directories"
17.2.3 Глубокая вложенность (20+ уровней)
- Max визуальный indent: 12 уровней (
min(level, 12) * 12px) - Tooltip на глубоких узлах с полным путём
buildTreeколлапс однодетных папок
17.2.4 Очень длинные имена файлов
- File tree:
truncate+titletooltip - Табы: max-width ~160px, modified dot ПЕРЕД текстом
- Breadcrumb: средние сегменты
...
17.2.5 Ошибка чтения файла (EACCES, ENOENT)
Показать: иконка AlertTriangle + текст ошибки + [Retry] + [Close Tab]
17.2.6 Resizable sidebar
- Default: 240px, min 160px, max 50% viewport
- Drag handle:
cursor-col-resize - Persist в localStorage
Cmd+Btoggle sidebar
17.3 Nice to Have (после MVP)
| Фича | Приоритет |
|---|---|
Cmd+Shift+P Command Palette (cmdk) |
P2 |
| Split View (Cmd+) | P3 |
| Minimap | P4 |
| Drag & Drop файлов | P4 |
| Indent guides | P2 |
| Find & Replace (Cmd+H) | P2 |
| Auto-save draft | P2 |
17.4 Правки к существующим секциям
- Секция 1.1 -- добавить:
EditorStatusBar.tsx,EditorBinaryState.tsx - Секция 2.1 -- добавить
hasUnsavedChangescomputed getter - Секция 2.3 -- добавить
disambiguatedLabel?: string - Секция 7.2 -- добавить "Close overlay with unsaved changes" (три кнопки)
- Секция 9.2 -- добавить
isBinaryвReadFileResult - Секция 11 --
Cmd+[/]->Cmd+Shift+[/]; добавитьCmd+B,Cmd+G - Секция 14 -- обновить: ~14 файлов вместо ~12
18. Security Review
Полный аудит безопасности. Проведён на основе анализа существующих паттернов проекта (
pathValidation.ts,validation.ts,review.ts,preload/index.ts) и 8 планируемых IPC каналов editor.
SEC-1: Path Traversal -- использовать validateFilePath (Critical)
Уязвимость: Каждый из 8 IPC каналов принимает путь от renderer. Скомпрометированный renderer может отправить ../../etc/passwd или /etc/shadow.
Текущий статус: Секция 4.2 уже исправлена -- описывает validateFilePath() вместо кастомного assertInsideRoot(). Хорошо.
Дополнительные требования:
- Для
editor:rename-- валидировать ОБА пути (oldPath и newPath) - Для
editor:readDir-- валидировать dirPath и КАЖДЫЙ обнаруженный entry - Не доверять конструкции
path.join(projectRoot, relativePath)без последующей проверки -- это не защищает отpath.join('/project', '/etc/passwd')(абсолютный путь перезаписывает base)
SEC-2: Symlink Resolution при рекурсивном обходе (Critical)
Уязвимость: readDir рекурсивно обходит директорию. Если внутри проекта symlink ./data -> /etc/, readDir вернёт содержимое /etc/.
Решение: В safeReadDir() для каждого entry проверять entry.isSymbolicLink(). Если да -- fs.realpath() + validateFilePath() на resolved target. Молча пропускать symlinks, ведущие за пределы projectRoot.
SEC-3: TOCTOU Race Condition (High)
Уязвимость: Между validateFilePath(path) и fs.readFile(path) файл может быть заменён на symlink к sensitive файлу.
Решение: После fs.readFile() повторно fs.realpath() + validateFilePath() (post-read verification). Для записи: atomic write через tmp + rename(). Вероятность эксплуатации в desktop-app низкая, но импакт критический.
SEC-4: File Size DoS / Device Files (High)
Уязвимость: Чтение /dev/zero (бесконечный поток нулей) или огромных файлов. Device файлы показывают size = 0 в stat.
Текущий статус: Секция 4.3 исправлена -- лимит 2MB, проверка isFile(), блокировка /dev/, /proc/, /sys/.
SEC-5: projectRoot НЕ от renderer (High)
Уязвимость: Скомпрометированный renderer отправляет projectRoot = '/' и обходит все проверки.
Решение: При stateless-подходе (секция 16.3): projectRoot хранится в module-level let activeProjectRoot в editor.ts. Устанавливается через editor:open(projectPath) (с валидацией). IPC handlers берут rootPath из module-level state, НЕ принимают от renderer.
SEC-6: Credential Leakage через readDir (Medium)
Уязвимость: .env, credentials.json, *.key видны в дереве. validateFilePath() блокирует readFile, но readDir покажет имена.
Решение: Показывать в дереве с визуальной пометкой (иконка замка). При клике -- "Sensitive file, cannot open in editor". Рассмотреть расширение SENSITIVE_PATTERNS: *.p12, *.pfx, serviceAccountKey.json.
SEC-7: XSS через имена файлов (Medium)
Уязвимость: Имя <script>alert(1)</script>.txt безопасно в React JSX, но опасно в document.title, tooltip с raw HTML, или window.open() title.
Решение: Рендерить имена только через JSX {fileName}. При создании: validateFileName() в main process -- запрет control characters (\x00-\x1f), path separators (/\:), имён . и .., длины > 255.
SEC-8: ReDoS в searchInFiles (Medium, итерация 4)
Уязвимость: Malicious regex (a+)+$ вызывает catastrophic backtracking в main process.
Решение: Только literal string search. Если regex нужен -- re2 engine или worker_thread с timeout. Лимит: max 1000 файлов, max 1MB на файл.
SEC-9: Atomic Write (Medium)
Решение: Write в tmp файл (${dir}/.tmp.${basename}.${pid}.${Date.now()}) + rename(). Cleanup tmp при ошибке. rename() атомарен только на одном filesystem -- tmp в той же директории обязательно.
SEC-10: editor:rename -- двойная валидация (High)
Уязвимость: Если валидируется только oldPath, можно переименовать файл ЗА ПРЕДЕЛЫ проекта или перезаписать чужой файл.
Решение: Валидировать ОБА пути через validateFilePath(). Проверить что newPath не существует (не перезаписывать). Валидировать новое имя файла.
SEC-11: СУЩЕСТВУЮЩАЯ уязвимость в review.ts (Critical, existing!)
ВНИМАНИЕ: handleSaveEditedFile в src/main/ipc/review.ts (строка 254) принимает filePath от renderer и передаёт в ReviewApplierService.saveEditedFile() (строка 320 ReviewApplierService.ts), который вызывает writeFile(filePath, content, 'utf8') БЕЗ КАКОЙ-ЛИБО ВАЛИДАЦИИ ПУТИ. Скомпрометированный renderer может записать произвольный файл куда угодно в файловой системе.
Решение: Добавить validateFilePath() в handleSaveEditedFile ДО записи. Это нужно исправить КАК МОЖНО СКОРЕЕ, НЕЗАВИСИМО от editor-фичи, как отдельный hotfix.
SEC-12: Запрет записи в .git/ (Medium)
Уязвимость: Модификация файлов в .git/ (особенно hooks/, config) может привести к произвольному выполнению кода при git commit/push/pull.
Решение: В ProjectFileService.writeFile/createFile/rename -- проверка что target path не внутри .git/ директории. Чтение .git/ -- можно разрешить (для информации), запись -- запретить.
SEC-13: IPC Rate Limiting (Low)
Уязвимость: Скомпрометированный renderer спамит IPC вызовами, вызывая disk I/O saturation.
Решение: Debounce на renderer (уже запланирован). На main process: простой counter -- max 100 вызовов/секунду. AbortController для отмены предыдущего readDir при новом запросе.
Сводная таблица уязвимостей
| ID | Уязвимость | Критичность | Статус |
|---|---|---|---|
| SEC-1 | Path traversal через IPC | Critical | Исправлено в секции 4.2 |
| SEC-2 | Symlink escape в readDir | Critical | Нужно добавить в реализацию |
| SEC-3 | TOCTOU race condition | High | Нужно добавить post-read verify |
| SEC-4 | File size / device DoS | High | Исправлено в секции 4.3 |
| SEC-5 | projectRoot от renderer | High | Нужно зафиксировать в module-level state |
| SEC-6 | Credential leakage | Medium | Частично покрыто validateFilePath |
| SEC-7 | XSS через имена файлов | Medium | React JSX покрывает, нужна validateFileName |
| SEC-8 | ReDoS в поиске | Medium | Нужно literal search, не regex |
| SEC-9 | Non-atomic write | Medium | Нужен tmp+rename |
| SEC-10 | rename двойная валидация | High | Нужно при реализации |
| SEC-11 | review.ts без валидации | Critical | СУЩЕСТВУЮЩИЙ БАГ, нужен hotfix |
| SEC-12 | Запись в .git/ | Medium | Нужно при реализации |
| SEC-13 | IPC rate limiting | Low | Optional |
Чеклист для реализации каждого IPC handler
[ ] validateFilePath(path, projectRoot) ДО файловой операции
[ ] projectRoot из module-level state, НЕ из параметров renderer
[ ] fs.lstat() + isFile()/isDirectory() перед чтением
[ ] stats.size <= MAX_FILE_SIZE (2MB) перед чтением
[ ] Buffer.byteLength(content) <= MAX_WRITE_SIZE (2MB) перед записью
[ ] Для rename: ОБА пути валидируются
[ ] Для readDir: каждый entry + symlinks проверяются
[ ] validateFileName() при создании файлов
[ ] Логирование через createLogger('IPC:editor')
[ ] Обёртка в wrapHandler -> IpcResult<T>
[ ] Device paths (/dev/, /proc/, /sys/) блокируются
[ ] Запись в .git/ запрещена
[ ] Post-read realpath verify (TOCTOU mitigation)
19. Performance Review
Аудит производительности по 9 направлениям. Основан на анализе реального кода: CodeMirrorDiffView.tsx (EditorView lifecycle, initialState, langCompartment), MembersJsonEditor.tsx (CM6 create/destroy), FileWatcher.ts (fs.watch patterns), changeReviewSlice.ts (file content caching), virtual scrolling в DateGroupedSessions/ChatHistory/NotificationsView.
19.1 Memory Leaks -- EditorView lifecycle (Impact: CRITICAL)
Проблема: План (секция 6.5) предлагает Map<tabId, EditorView> + CSS show/hide (display: none/block). При 20+ табах это 20 живых EditorView в DOM:
- DocumentTree ~2x размер файла
- DOM MutationObserver, ResizeObserver, event listeners на каждом
- Incremental parse tree языкового парсера
- 1MB файл = ~15-25MB RAM на EditorView
- 20 табов x 500KB = ~400-500MB RAM
В проекте сейчас: CodeMirrorDiffView.tsx (строки 694-717) корректно вызывает view.destroy() в cleanup. MembersJsonEditor.tsx (строки 68-71) аналогично. Оба пересоздают EditorView, НЕ скрывают.
Решение (ОБЯЗАТЕЛЬНАЯ замена): EditorState pooling + single EditorView:
1. Map<tabId, EditorState> в useRef (НЕ EditorView, НЕ Zustand)
2. Один активный EditorView на весь редактор
3. Переключение таба:
a. savedStates.set(oldTabId, view.state) // undo, cursor, selection
b. currentView.destroy()
c. new EditorView({ state: savedStates.get(newTabId), parent: container })
4. Закрытие таба: savedStates.delete(tabId)
5. Паттерн initialState уже есть в CodeMirrorDiffView (строка 699-705)
Память: EditorState ~1.5x документа (JS only) vs EditorView ~10-15x (DOM). Экономия ~8-12x.
LRU при >30 states: вытеснять oldest, сохраняя doc.toString() + cursor (без undo).
Benchmark: 25 файлов x 200KB. performance.memory.usedJSHeapSize: CSS hide ~500MB vs pooling ~80-120MB.
19.2 CSS show/hide vs re-mount (Impact: CRITICAL)
Проблема (секция 6.5): "show/hide через CSS" -- неправильно:
- 20 EditorView = огромный DOM tree
display: noneНЕ отключает observers- requestMeasure() продолжает вызываться
- При
display: block-- пересчёт высот строк (LAG)
Re-mount из EditorState: 100KB файл ~3-5ms, undo сохраняется, scroll восстанавливается через EditorView.scrollIntoView(pos).
Решение: Заменить секцию 6.5:
1. Один EditorView, один DOM-контейнер, один активный файл
2. Map<tabId, EditorState> в useRef
3. save state -> destroy -> new view from saved state
4. Dirty flag через debounced updateListener (300ms)
5. LRU eviction при > 30 states
19.3 IPC Bottlenecks -- readDir/readFile (Impact: HIGH)
Проблема: readDir 10,000+ файлов: JSON 500KB-2MB, main thread 50-200ms. readFile 5MB: structured clone ~30-100ms.
A. readDir -- усиленный lazy loading:
- Только root level при открытии
- expand -> readDir(path, depth=1)
- MAX_ENTRIES_PER_DIR = 500 (не 10,000)
- >500: "N more files..." + "Show all"
- Prefetch при hover (debounced 200ms)
B. readFile -- тиерная стратегия:
- <256KB: мгновенно
- 256KB-2MB: progress indicator
- 2MB-5MB: preview (100 строк + warning)
- >5MB: external editor (shell:openPath)
C. Main process: AbortSignal, concurrency limit=3, дедупликация.
Benchmark: 5000 файлов -> дерево < 200ms.
19.4 React Re-renders -- keystroke storm (Impact: HIGH)
Проблема: editorModifiedContents: Record<string, string> -- каждый keystroke -> set() -> новый объект -> все подписчики рендерятся.
Решение -- НЕ хранить content в Zustand:
// Контент ТОЛЬКО в EditorState CodeMirror
// Zustand: editorModifiedFiles: Set<string> // только dirty flags
// save: savedEditorStates.get(path)?.doc.toString()
0 keystroke re-renders. Dirty flag debounced 300ms (паттерн из CodeMirrorDiffView строки 517-527).
Гранулярные селекторы:
const tabList = useStore(s => s.editorOpenTabs, shallow);
const activeId = useStore(s => s.editorActiveTabId);
Benchmark: React DevTools Profiler. FileTreePanel/TabBar НЕ рендерятся при наборе.
19.5 File Tree -- виртуализация (Impact: HIGH)
Проблема: 5000+ рекурсивных FileTreeNode = 200-500ms render.
Фаза 1 (итерации 1-2): Lazy loading + MAX_VISIBLE_NODES=1000 + auto-collapse.
Фаза 2 (итерация 4): @tanstack/react-virtual (уже в проекте -- DateGroupedSessions, ChatHistory, NotificationsView):
flattenTree(tree, expandedDirs) -> FlatNode[]
useVirtualizer({ count, estimateSize: () => 28 })
Benchmark: lodash src, все папки раскрыты. FPS скролла через Chrome DevTools.
19.6 Large Files -- минификация (Impact: MEDIUM)
CM6 virtual scrolling по СТРОКАМ. Одна строка 1MB = один DOM-элемент = LAG.
Трёхуровневая защита:
Размер: <500KB полный | 500KB-2MB без syntax | 2MB-5MB read-only | >5MB external
Строки: >10,000 chars -> banner "Minified" + Pretty-print/lineWrapping
Binary: null bytes в первых 8KB или расширение (.png, .wasm)
19.7 Concurrent Operations (Impact: MEDIUM)
10 быстрых кликов = 10 параллельных readFile.
Решение: Дедупликация через Map<string, Promise> + concurrency limit=3 в main process.
19.8 File Watcher (Impact: MEDIUM)
Проект использует fs.watch({ recursive: true }), не chokidar. Electron 40/Node 20+ OK.
Решение: fs.watch + фильтр (node_modules/.git/dist) + debounce 200ms + opt-in (ручной F5 по умолчанию) + cleanup.
19.9 Bundle Size (Impact: LOW)
Все CM6 пакеты установлены. Нужен только @codemirror/search (~15KB gzipped). Незначительно.
Сводная таблица
| # | Проблема | Impact | Итерация | Статус в плане |
|---|---|---|---|---|
| 19.1 | EditorView memory 20+ табов | CRITICAL | 1 | НЕВЕРНО -- EditorState pooling |
| 19.2 | CSS show/hide vs re-mount | CRITICAL | 1 | НЕВЕРНО -- single EditorView |
| 19.3 | IPC readDir/readFile | HIGH | 1 | Частично -- тиеры + очередь |
| 19.4 | Zustand keystroke re-renders | HIGH | 2 | НЕ покрыт -- content вне store |
| 19.5 | FileTree без виртуализации | HIGH | 4 | НЕ покрыт -- react-virtual |
| 19.6 | Минификация/длинные строки | MEDIUM | 1 | Частично -- 3 уровня |
| 19.7 | Concurrent readFile | MEDIUM | 1 | НЕ покрыт -- дедупликация |
| 19.8 | fs.watch overhead | MEDIUM | 5 | OK, но opt-in |
| 19.9 | Bundle size | LOW | 1 | OK |