- 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.
42 KiB
Анализ переиспользования кодовой базы для In-App Project Editor
1. Переиспользуемые компоненты
1.1 ReviewFileTree -- высокий потенциал извлечения
Файл: /Users/belief/dev/projects/claude/claude_team/src/renderer/components/team/review/ReviewFileTree.tsx
Это самый важный компонент для переиспользования. Внутри него:
buildTree(files)функция (строки 42-83) -- построение дерева из плоского списка путей. Алгоритм: разбивает пути по/, строит иерархиюTreeNode, коллапсирует одноуровневые папки (как в VS Code). Это универсальный алгоритм, не привязанный к review.TreeItemкомпонент (строки 147-264) -- рекурсивный рендеринг узла дерева с иконками, отступами, коллапсом папок.ReviewFileTree(строки 297-376) -- корневой компонент с auto-expand и auto-scroll.
Проблема: Сейчас ReviewFileTree жестко привязан к review-контексту:
TreeItemпринимаетhunkDecisions,fileDecisions,fileChunkCounts,viewedSet-- всё review-специфичноеFileStatusIconрендерит статусы review (accepted/rejected/mixed/pending)- Строки +/- в каждом файле (
linesAdded,linesRemoved)
Рекомендация: Извлечь generic FileTree из ReviewFileTree. Структура:
- Выделить
buildTree()иTreeNodeв утилитуsrc/renderer/utils/fileTreeBuilder.ts - Создать generic
FileTreeкомпонент сrenderItemcallback (render-prop для кастомизации правой части каждого файлового элемента) ReviewFileTreeстановится тонкой обёрткой вокругFileTreeс review-специфичнымrenderItemEditorFileTree-- вторая обёртка для редактора (показывает иконки по типу файла, dirty-маркер)
Оценка надёжности: 8/10 -- buildTree проверен в продакшене, алгоритм коллапса протестирован. Оценка уверенности: 9/10 -- это чистый extract-and-wrap рефакторинг.
1.2 CodeMirrorDiffView -- частичное переиспользование
Файл: /Users/belief/dev/projects/claude/claude_team/src/renderer/components/team/review/CodeMirrorDiffView.tsx
Этот компонент содержит ценную инфраструктуру:
getSyncLanguageExtension(fileName)(строки 64-123) -- маппинг расширений файлов на CodeMirror language extensions. 16+ языков. Должен быть извлечён в общую утилиту.getAsyncLanguageDesc(fileName)(строки 126-128) -- async fallback через@codemirror/language-data.diffTheme(строки 158-283) -- тема CodeMirror на CSS-переменных. Частично переиспользуема для обычного редактора (базовые стили.cm-gutters,.cm-content,.cm-scroller).langCompartmentпаттерн -- Compartment для ленивой инжекции языка. Полностью переиспользуем.buildExtensions()(строки 477-688) -- настройка расширений. Для редактора нужна упрощённая версия (без merge/diff, без hunk navigation).
Что НЕ переиспользуется: Вся diff/merge логика (unifiedMergeView, mergeCompartment, chunk navigation, merge toolbar) -- это 60%+ кода компонента.
Рекомендация: Создать CodeMirrorEditor компонент (без diff) рядом или вместо fork'а CodeMirrorDiffView:
- Извлечь
getLanguageExtension()вsrc/renderer/utils/codemirrorLanguages.ts - Извлечь базовую тему в
src/renderer/utils/codemirrorTheme.ts - Новый
CodeMirrorEditorиспользует эти утилиты +@codemirror/autocomplete(уже вpackage.json!)
Оценка надёжности: 7/10 -- ядро проверено, но отделение от diff-логики требует внимания. Оценка уверенности: 8/10 -- чётко понятно что извлекать.
1.3 ChangeReviewDialog -- паттерн layout
Файл: /Users/belief/dev/projects/claude/claude_team/src/renderer/components/team/review/ChangeReviewDialog.tsx
Это полноэкранный overlay (не Radix Dialog!). Паттерн (строки 507-676):
fixed inset-0 z-50 flex flex-col bg-surface
├── Header (border-b, bg-surface-sidebar, macOS traffic-light padding)
├── Toolbar (border-b)
└── Content (flex flex-1 overflow-hidden)
├── Sidebar (w-64, overflow-y-auto, border-r, bg-surface-sidebar)
└── Main content area (flex-1)
Что переиспользуется:
- Layout паттерн: header + sidebar + content
- macOS traffic-light padding (
--macos-traffic-light-padding-left,WebkitAppRegion: 'drag') - Escape-to-close (строки 346-353)
- Loading/Error/Empty states (строки 586-673)
Рекомендация: Создать FullScreenPanel layout-компонент, который предоставляет:
- Header slot с macOS-safe padding
- Optional sidebar slot
- Content slot
- Escape-to-close behaviour
- Loading/Error/Empty state handling
Или проще -- просто скопировать layout-паттерн в ProjectEditor, а рефакторить в общий компонент потом.
Оценка надёжности: 7/10 Оценка уверенности: 7/10 -- зависит от того, насколько сильно отличается layout редактора.
1.4 DiffErrorBoundary -- прямое переиспользование
Файл: /Users/belief/dev/projects/claude/claude_team/src/renderer/components/team/review/DiffErrorBoundary.tsx
Специализированный error boundary для diff-view. Нужен аналогичный для CodeMirror editor. Можно обобщить:
- Переименовать в
EditorErrorBoundary - Убрать diff-специфичные пропы (
oldString,newString) - Добавить generic error info display
Оценка надёжности: 9/10 Оценка уверенности: 9/10
1.5 UI примитивы
Прямое переиспользование без изменений:
| Компонент | Путь | Применение |
|---|---|---|
ErrorBoundary |
src/renderer/components/common/ErrorBoundary.tsx |
Обёртка всего редактора |
CopyablePath |
src/renderer/components/common/CopyablePath.tsx |
Путь к файлу в header |
CopyButton |
src/renderer/components/common/CopyButton.tsx |
Копирование содержимого |
ConfirmDialog |
src/renderer/components/common/ConfirmDialog.tsx |
"Save before close?" |
Tooltip |
src/renderer/components/ui/tooltip.tsx |
Тултипы на кнопках toolbar |
Button |
src/renderer/components/ui/button.tsx |
Кнопки toolbar |
Dialog |
src/renderer/components/ui/dialog.tsx |
Мелкие модалки (settings) |
Tabs |
src/renderer/components/ui/tabs.tsx |
Табы открытых файлов |
1.6 Компоненты review, которые НЕ стоит переиспользовать
ReviewToolbar-- слишком review-специфичен (accept/reject/apply counters)ContinuousScrollView-- scroll-spy для diff-review, не подходит для редактораFileSectionDiff/FileSectionHeader-- привязаны к diff workflowViewedProgressBar-- review-onlyConflictDialog-- review-only
2. Существующие IPC каналы
2.1 Уже есть -- файловые операции
| Канал | Файл | Что делает | Применимость |
|---|---|---|---|
review:saveEditedFile |
src/main/ipc/review.ts |
Сохраняет файл на диск (filePath, content) |
УЯЗВИМОСТЬ: нет валидации пути! НЕ переиспользовать без исправления (см. SEC-11). Для editor -- отдельный канал с валидацией |
review:getFileContent |
src/main/ipc/review.ts |
Читает файл + original + modified | Частично -- нужна упрощённая версия |
read-mentioned-file |
src/main/ipc/utility.ts |
Читает файл по абсолютному пути с валидацией | Можно использовать, но ограничен maxTokens |
shell:openPath |
src/main/ipc/utility.ts |
Открывает файл в системном приложении | "Open in external editor" |
shell:showInFolder |
src/main/ipc/utility.ts |
Показывает файл в Finder | "Reveal in Finder" |
2.2 Чего НЕТ -- нужно создать
Для полноценного редактора проекта нужны новые IPC каналы:
editor:listDirectory(dirPath)-- рекурсивный listing файлов (с ignore-паттернами:.git,node_modules, etc.)editor:readFile(filePath)-- чтение файла без ограниченийmaxTokens(в отличие отread-mentioned-file)editor:saveFile(filePath, content)-- можно переиспользоватьreview:saveEditedFile, но лучше отдельный канал с более широкой валидациейeditor:createFile(filePath, content?)-- создание нового файлаeditor:deleteFile(filePath)-- удаление файла (сconfirmна renderer стороне)editor:renameFile(oldPath, newPath)-- переименованиеeditor:watchDirectory(dirPath)-- подписка на изменения в директории (для обновления file tree)
Паттерн регистрации (из src/main/ipc/review.ts):
// Module-level state + guard
let service: EditorService | null = null;
function getService(): EditorService { ... }
// Forward-compatible config object
export interface EditorHandlerDeps { ... }
export function initializeEditorHandlers(deps: EditorHandlerDeps): void { ... }
export function registerEditorHandlers(ipcMain: IpcMain): void { ... }
export function removeEditorHandlers(ipcMain: IpcMain): void { ... }
Каналы в ipcChannels.ts -- плоские export const, НЕ объект (подтверждено в MEMORY.md).
Оценка надёжности: 8/10 -- паттерн отработан на 20+ модулях. Оценка уверенности: 9/10
3. Zustand-паттерн для Editor Slice
3.1 Существующий паттерн slice'ов
Файл: /Users/belief/dev/projects/claude/claude_team/src/renderer/store/types.ts
18 slice'ов, объединённых через intersection type. Каждый slice:
export interface SomeSlice {
// Data
someData: T[];
selectedId: string | null;
loading: boolean;
error: string | null;
// Actions
fetchData: () => Promise<void>;
selectItem: (id: string | null) => void;
}
export const createSomeSlice: StateCreator<AppState, [], [], SomeSlice> = (set, get) => (
// initial state + actions
});
3.2 Рекомендуемая структура EditorSlice
export interface EditorSlice {
// State
editorProjectPath: string | null; // Текущий проект
editorFileTree: FileTreeNode[]; // Дерево файлов
editorFileTreeLoading: boolean;
editorOpenFiles: OpenFile[]; // Открытые файлы (табы)
editorActiveFilePath: string | null; // Активный файл
editorDirtyFiles: Set<string>; // Файлы с несохранёнными изменениями
editorError: string | null;
// File content cache (path -> content)
editorFileContents: Record<string, string>;
editorFileContentsLoading: Record<string, boolean>;
// Actions
openEditor: (projectPath: string) => Promise<void>;
closeEditor: () => void;
loadFileTree: (dirPath: string) => Promise<void>;
openFile: (filePath: string) => Promise<void>;
closeFile: (filePath: string) => void;
setActiveFile: (filePath: string) => void;
updateFileContent: (filePath: string, content: string) => void;
saveFile: (filePath: string) => Promise<void>;
saveAllDirty: () => Promise<void>;
}
Важно: Следовать правилу из CLAUDE.md -- "Store over Props": дочерние компоненты читают из store напрямую через useStore().
Куда добавить:
src/renderer/store/slices/editorSlice.ts-- новый slice- Добавить
EditorSliceвAppStatetype вtypes.ts - Добавить
...createEditorSlice(...args)вstore/index.ts
Оценка надёжности: 9/10 Оценка уверенности: 9/10
3.3 Ближайший аналог -- changeReviewSlice
Файл: /Users/belief/dev/projects/claude/claude_team/src/renderer/store/slices/changeReviewSlice.ts
Этот slice ближе всего к будущему editorSlice:
fileContents: Record<string, FileChangeWithContent>-- кеш содержимого файловfileContentsLoading: Record<string, boolean>-- состояние загрузки per-fileeditedContents: Record<string, string>-- несохранённые измененияsaveEditedFile(filePath)-- сохранение на дискdiscardFileEdits(filePath)-- отмена изменений- Debounced persistence
4. CSS/Theme -- переиспользование
4.1 Существующие CSS-переменные
Файл: /Users/belief/dev/projects/claude/claude_team/src/renderer/index.css
Полностью подходят для редактора:
| Категория | Переменные | Применение в редакторе |
|---|---|---|
| Surfaces | --color-surface, --color-surface-raised, --color-surface-sidebar |
Фон редактора, sidebar, header |
| Borders | --color-border, --color-border-subtle, --color-border-emphasis |
Разделители панелей |
| Text | --color-text, --color-text-secondary, --color-text-muted |
Текст в file tree, status bar |
| Code | --code-bg, --code-border, --code-line-number, --code-filename |
Фон редактора, номера строк |
| Syntax | --syntax-string, --syntax-comment, --syntax-keyword и т.д. |
Подсветка синтаксиса |
| Inline code | --inline-code-bg, --inline-code-text |
Инлайн код в markdown |
| Scrollbar | --scrollbar-thumb, --scrollbar-thumb-hover |
Скроллбар в file tree |
| Card | --card-bg, --card-border, --card-header-bg |
Панели, headers |
| Skeleton | --skeleton-base, --skeleton-base-light |
Loading state |
4.2 Тема CodeMirror
diffTheme в CodeMirrorDiffView.tsx (строки 158-283) уже использует CSS-переменные:
'&': {
backgroundColor: 'var(--color-surface)',
color: 'var(--color-text)',
fontFamily: 'ui-monospace, SFMono-Regular, ...',
fontSize: '13px',
},
'.cm-gutters': {
backgroundColor: 'var(--color-surface)',
borderRight: '1px solid var(--color-border)',
...
}
Нужно извлечь базовую тему (без diff-стилей .cm-changedLine, .cm-deletedChunk и т.д.) -- примерно 40% от текущей темы.
4.3 Light theme
Поддержка есть через :root.light override'ы в index.css. Если diffTheme использует CSS-переменные (а он использует), то light theme заработает автоматически.
5. CodeMirror vs ProseMirror
5.1 CodeMirror 6 -- уже в проекте
Из package.json:
"@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.2",
"@codemirror/lang-cpp", "@codemirror/lang-css", "@codemirror/lang-go",
"@codemirror/lang-html", "@codemirror/lang-java", "@codemirror/lang-javascript",
"@codemirror/lang-json", "@codemirror/lang-less", "@codemirror/lang-markdown",
"@codemirror/lang-php", "@codemirror/lang-python", "@codemirror/lang-rust",
"@codemirror/lang-sass", "@codemirror/lang-sql", "@codemirror/lang-xml",
"@codemirror/lang-yaml",
"@codemirror/language", "@codemirror/language-data",
"@codemirror/merge", "@codemirror/state",
"@codemirror/theme-one-dark", "@codemirror/view"
Это 16 языковых пакетов + @codemirror/language-data (ещё ~30 языков async). Плюс @codemirror/autocomplete уже установлен.
5.2 Рекомендация: ТОЛЬКО CodeMirror 6
Однозначно CodeMirror 6, НЕ ProseMirror. Причины:
- Уже 20+ пакетов CodeMirror в зависимостях -- нулевой overhead по bundle size
- Работающая инфраструктура:
getSyncLanguageExtension(),getAsyncLanguageDesc(), тема, Compartment-паттерн -- всё протестировано в production @codemirror/autocompleteуже установлен -- автодополнение из коробки- CodeMirror = код-редактор, ProseMirror = rich text / WYSIWYG. Для проектного редактора нужен именно код-редактор
- ProseMirror добавил бы ~150-200KB в bundle + совершенно новая экосистема плагинов
Не нужно добавлять НИКАКИХ новых зависимостей для базового редактора. Всё есть.
Оценка надёжности: 10/10 -- CodeMirror 6 зрелый, используется в VSCode, Chrome DevTools Оценка уверенности: 10/10 -- ProseMirror для code editing = антипаттерн
6. Anti-patterns и риски
6.1 Размер компонентов
Проблема: ChangeReviewDialog.tsx -- 677 строк. CodeMirrorDiffView.tsx -- 809 строк. Оба на грани допустимого.
Рекомендация для Editor:
ProjectEditor.tsx-- max 150 строк (layout shell, делегирует всё дочерним)EditorFileTree.tsx-- max 200 строкEditorTabBar.tsx-- max 100 строкEditorCodePane.tsx-- max 150 строк (обёртка вокруг CodeMirror)EditorToolbar.tsx-- max 100 строк- Хуки (
useEditorKeyboard,useEditorFileOps) -- по 50-100 строк
6.2 Performance с большими файлами
Проблема: CodeMirror 6 virtual scrolling работает, но:
- Файлы >5MB могут замедлить парсинг языка
readFileчерез IPC сериализует содержимое как JSON string -- большие файлы замедляют IPC
Рекомендация:
- Лимит чтения: ~2MB (показывать "File too large, open externally")
EditorView.scrollPastEnd-- чтобы пользователь мог скроллить ниже конца файла- Lazy language loading через Compartment (уже реализовано в
CodeMirrorDiffView)
6.3 Dirty state и unsaved changes
Проблема: changeReviewSlice хранит editedContents как Record<string, string> -- весь контент файла в памяти per-dirty-file. При 10+ грязных файлах это может быть гигабайт RAM.
Рекомендация:
- Хранить ТОЛЬКО для активного файла + 2-3 соседних табов (LRU cache)
- Для остальных -- хранить
EditorStateобъект CodeMirror (он уже в памяти CM) - При переключении табов -- сохранять
EditorState(включая undo history), не строку
6.4 File watching race conditions
Проблема: Если пользователь редактирует файл в нашем редакторе, а CLI-агент одновременно меняет его через review:saveEditedFile -- конфликт.
Рекомендация:
mtimecheck перед записью (какreview:checkConflict)- Уведомление "File changed on disk" с выбором (reload / keep mine / show diff)
6.5 Missing error boundaries
Проблема: ErrorBoundary в common/ -- один на всё приложение. DiffErrorBoundary -- только для diff. Если CodeMirror крашится в editor mode, нужен отдельный boundary.
Рекомендация: Обернуть CodeMirrorEditor в специализированный EditorErrorBoundary (можно обобщить DiffErrorBoundary).
6.6 IPC parameter validation
Проблема (CRITICAL): В review.ts IPC handler handleSaveEditedFile НЕ валидирует путь -- прямой writeFile() без validateFilePath(). Это существующая уязвимость (см. секцию 10.3).
Рекомендация:
- ВСЕ IPC handlers, работающие с файлами, ОБЯЗАНЫ вызывать
validateFilePath()изsrc/main/utils/pathValidation.ts - Для editor: выделенный module-level
activeProjectRoot, не принимаемый от renderer при каждом вызове - Дополнительно:
validateFileName()для создания файлов,isDevicePath()для блокировки device files, запрет записи в.git/ - Подробный чеклист -- в
plan-architecture.mdсекция 18
7. Итоговая архитектурная рекомендация
Что ИЗВЛЕЧЬ из существующего кода (рефакторинг):
buildTree()+TreeNode-->src/renderer/utils/fileTreeBuilder.tsgetSyncLanguageExtension()+getAsyncLanguageDesc()-->src/renderer/utils/codemirrorLanguages.ts- Базовая CM тема (без diff) -->
src/renderer/utils/codemirrorTheme.ts ReviewFileTree--> genericFileTree+ReviewFileTreewrapper
Что СОЗДАТЬ с нуля:
src/renderer/store/slices/editorSlice.tssrc/main/ipc/editor.ts+ handler'ыsrc/preload/constants/ipcChannels.ts-- добавитьEDITOR_*каналыsrc/preload/index.ts-- добавитьeditorAPIsrc/renderer/components/editor/-- компоненты редактораsrc/main/services/editor/EditorService.ts-- сервис файловых операций
Что ПЕРЕИСПОЛЬЗОВАТЬ напрямую:
- Все UI примитивы из
components/ui/ ErrorBoundary,ConfirmDialog,CopyablePath,CopyButton- CSS-переменные (100% готовы)
- CodeMirror 6 пакеты (все 20+ уже в зависимостях)
wrapHandler<T>()паттерн для IPC- Zustand slice pattern
8. Архитектурная ревизия: дополнения к reuse-анализу
Добавлено после ревизии. Конкретизирует что именно извлекать и как.
8.1 Обязательные рефакторинги перед реализацией
Эти рефакторинги -- не optional. Без них будет дублирование кода, нарушающее DRY:
| Что извлечь | Откуда | Куда | Строки |
|---|---|---|---|
buildTree() + collapse() + сортировка |
ReviewFileTree.tsx:42-83 |
src/renderer/utils/fileTreeBuilder.ts |
~50 LOC |
getSyncLanguageExtension() + getAsyncLanguageDesc() |
CodeMirrorDiffView.tsx:64-128 |
src/renderer/utils/codemirrorLanguages.ts |
~70 LOC |
| Базовая тема CM (без diff-стилей) | CodeMirrorDiffView.tsx:158-198 |
src/renderer/utils/codemirrorTheme.ts |
~40 LOC |
wrapReviewHandler<T>() |
review.ts:133-145 |
src/main/ipc/ipcWrapper.ts |
~15 LOC |
Порядок: Рефакторинги 1-4 выполняются ПЕРЕД написанием нового кода итерации 1.
ReviewFileTree.tsx и CodeMirrorDiffView.tsx начинают импортировать из новых утилит.
Тесты этих компонентов должны продолжать проходить (zero behavior change).
8.2 Расхождения между файлами планов (исправлены)
| Расхождение | plan-architecture.md | plan-iterations.md | Решение |
|---|---|---|---|
| Имя сервиса | FileEditorService |
ProjectFileService |
ProjectFileService |
| Stateful/Stateless | constructor(rootPath) |
Не указано | Stateless, projectRoot как аргумент |
| Security | Свой assertInsideRoot() |
validateFilePath() |
validateFilePath() из pathValidation.ts |
| editorSlice в итерации 1 | Да | Нет (хук useEditorState) |
Нет slice в итерации 1, useState достаточно |
useEditorState.ts хук |
Не упомянут | Создаётся в итерации 2 | Убран, вся логика в slice |
| Overlay name | CodeEditorOverlay |
ProjectEditorOverlay |
ProjectEditorOverlay (лучше отражает scope) |
8.3 Review FileTree: конкретный план generic extraction
Текущий ReviewFileTree.tsx (~377 строк) содержит:
TreeNodeтип -- generic (name, fullPath, isFile, children, file?)buildTree()-- generic (принимаетfilesс.relativePath)collapse()-- generic (одноуровневый collapse)TreeItem-- review-specific (FileStatusIcon, +/- lines, viewedSet, hunkDecisions)getFileStatus()-- review-specificReviewFileTree-- review-specific (reads from store: hunkDecisions, fileDecisions)
Plan для generic FileTree:
src/renderer/utils/fileTreeBuilder.ts:
- export type TreeNode<T = unknown> = { name, fullPath, isFile, data?: T, children }
- export function buildTree<T>(items: T[], getRelativePath: (item: T) => string): TreeNode<T>[]
- export function sortTreeNodes<T>(nodes: TreeNode<T>[]): TreeNode<T>[]
src/renderer/components/common/FileTree.tsx:
- Generic FileTree<T> component
- Props: nodes, activeNodePath, onNodeClick, renderNodeExtra?, renderNodeIcon?
- Internal: TreeItem (renders folder/file, delegation через render-props)
- Handles: collapsedFolders, toggleFolder, auto-expand ancestors, auto-scroll
src/renderer/components/team/review/ReviewFileTree.tsx:
- Thin wrapper around FileTree<FileChangeSummary>
- Provides renderNodeExtra with FileStatusIcon + +/- lines
- Reads hunkDecisions/fileDecisions from store
src/renderer/components/team/editor/EditorFileTree.tsx:
- Thin wrapper around FileTree<FileTreeEntry>
- Provides renderNodeExtra with dirty marker
- Provides renderNodeIcon with file type icons
- Context menu integration
8.4 SOLID compliance checklist
- SRP: FileTreePanel -- UI only, data loading in slice
- SRP: CodeMirrorEditor -- lifecycle only, extensions in builder
- OCP: FileTree -- generic with render-props
- LSP: FileTreeNode extends FileTreeEntry (no field duplication)
- ISP: EditorSlice split into 4 documented groups
- DIP: Extensions via factory, not hardcoded in component
- DRY: buildTree, language detection, theme, wrapHandler -- all extracted
- Clean Architecture: dependency flow verified, no backward deps
9. UX Review: дополнения к reuse-анализу
Добавлено после UX-ревью. Что ещё нужно переиспользовать/создать для качественного UX.
9.1 Дополнительные компоненты для переиспользования
| Компонент | Путь | Применение в редакторе |
|---|---|---|
KeyboardShortcutsHelp |
review/KeyboardShortcutsHelp.tsx |
Модальное окно со списком shortcuts (кнопка ? в header) |
confirm() imperative API |
common/ConfirmDialog.tsx |
"Save before close?" при Escape с unsaved changes |
9.2 Новые утилиты, вызванные UX-требованиями
| Утилита | Путь | Зачем |
|---|---|---|
tabLabelDisambiguation.ts |
src/renderer/utils/ |
Показ "(main/utils)" для дублей index.ts в табах |
binaryDetector.ts |
src/main/utils/ |
Определение бинарных файлов (null bytes в первых 8KB) |
9.3 Новые компоненты, вызванные UX-требованиями
| Компонент | Описание |
|---|---|
EditorStatusBar.tsx |
Нижняя полоска: Ln:Col, язык, отступы, кодировка |
EditorBinaryState.tsx |
Заглушка для бинарных файлов вместо CM6 |
EditorErrorState.tsx |
Заглушка для файлов с ошибкой чтения (EACCES, ENOENT) |
EditorShortcutsHelp.tsx |
Модальное окно shortcuts (или переиспользовать KeyboardShortcutsHelp) |
9.4 CSS-переменные -- что уже есть, чего не хватает
Уже есть (полностью достаточно):
--color-surface,--color-surface-sidebar,--color-surface-raised-- для background--color-border,--color-border-subtle,--color-border-emphasis-- для разделителей--color-text,--color-text-secondary,--color-text-muted-- для текста--code-*,--syntax-*-- для CodeMirror--scrollbar-*-- для скроллбара--card-*-- для панелей
Не хватает (рекомендация: добавить в :root в index.css):
/* Editor-specific */
--editor-tab-active-bg: var(--color-surface);
--editor-tab-inactive-bg: var(--color-surface-sidebar);
--editor-tab-modified-dot: #f59e0b; /* amber для modified indicator */
--editor-tab-border: var(--color-border);
--editor-statusbar-bg: var(--color-surface-sidebar);
--editor-statusbar-text: var(--color-text-muted);
--editor-sidebar-resize-handle: rgba(148, 163, 184, 0.15);
--editor-sidebar-resize-handle-hover: rgba(148, 163, 184, 0.3);
Это обеспечит консистентность с остальными CSS-переменными проекта и лёгкую кастомизацию.
9.5 Accessibility -- что переиспользовать из существующего
ReviewFileTree.tsx (строка 232) имеет aria-label на expand/collapse. Это МИНИМУМ. При извлечении generic FileTree нужно сразу добавить:
role="tree"на корневой<ul>role="treeitem"+aria-expandedна каждой папкеrole="group"на вложенных<ul>role="treeitem"+aria-selectedна файлах- Keyboard navigation (arrow keys) -- в
FileTree, не в обёртках
Это не "nice to have" -- это требование WCAG 2.1 Level A для tree view.
10. Security Review: дополнения к reuse-анализу
Полный аудит безопасности описан в
plan-architecture.mdсекция 18. Здесь -- что из существующего кода переиспользовать для безопасности, и обнаруженные проблемы в текущем коде.
10.1 Переиспользуемые security-утилиты
| Утилита | Путь | Что делает | Как использовать |
|---|---|---|---|
validateFilePath() |
src/main/utils/pathValidation.ts |
Path traversal, symlink escape, sensitive patterns | КАЖДЫЙ IPC handler ОБЯЗАН вызывать |
SENSITIVE_PATTERNS |
src/main/utils/pathValidation.ts |
Regex-массив: .env, .ssh, *.key, *.pem и т.д. |
Автоматически через validateFilePath() |
resolveRealPathIfExists() |
src/main/utils/pathValidation.ts |
fs.realpathSync.native() с обработкой ENOENT |
Автоматически через validateFilePath() |
isPathWithinAllowedDirectories() |
src/main/utils/pathValidation.ts |
Containment check с cross-platform support | Автоматически через validateFilePath() |
isPathContained() |
src/main/ipc/validation.ts |
Простая containment check (normalize + startsWith) | НЕ использовать отдельно -- validateFilePath полнее |
10.2 Чего НЕ хватает в существующих утилитах (нужно создать для editor)
| Утилита | Описание | Зачем |
|---|---|---|
validateFileName(name) |
Валидация имени файла при создании | Запрет ., .., control chars, path separators, NUL, length > 255 |
isDevicePath(path) |
Проверка на /dev/, /proc/, /sys/ |
Блокировка device files до fs.readFile() |
isGitInternalPath(path) |
Проверка на .git/ в пути |
Запрет записи в .git/ (чтение -- ОК) |
atomicWriteFile(path, content) |
Atomic write через tmp + rename | Защита от corrupt при crash/disk full |
Рекомендация: добавить в src/main/utils/pathValidation.ts (validateFileName, isDevicePath, isGitInternalPath) и src/main/utils/atomicWrite.ts (atomicWriteFile).
10.3 Обнаруженная уязвимость в review.ts (Critical, existing!)
При анализе review.ts (секция 2.1 reuse-анализа) обнаружена уязвимость:
handleSaveEditedFile (строка 254 review.ts) принимает filePath от renderer и передаёт в ReviewApplierService.saveEditedFile() (строка 320 ReviewApplierService.ts), который вызывает writeFile(filePath, content, 'utf8') БЕЗ КАКОЙ-ЛИБО ВАЛИДАЦИИ ПУТИ.
Текущий код:
// review.ts:254
async function handleSaveEditedFile(_event, filePath, content) {
if (!filePath || typeof content !== 'string') {
return { success: false, error: 'Invalid parameters' };
}
// УЯЗВИМОСТЬ: filePath НЕ проверяется через validateFilePath()
return wrapReviewHandler('saveEditedFile', async () => {
const result = await getApplier().saveEditedFile(filePath, content);
// ...
});
}
// ReviewApplierService.ts:320
async saveEditedFile(filePath: string, content: string) {
// УЯЗВИМОСТЬ: прямая запись без валидации
await writeFile(filePath, content, 'utf8');
return { success: true };
}
Импакт: Скомпрометированный renderer может записать произвольный файл куда угодно в ФС.
Решение: Добавить validateFilePath(filePath, projectRoot) в handleSaveEditedFile. Нужен hotfix НЕЗАВИСИМО от editor-фичи.
10.4 Security-паттерн для editor IPC (обязательный)
// src/main/ipc/editor.ts -- каждый handler ОБЯЗАН следовать этому паттерну:
let activeProjectRoot: string | null = null; // module-level, set by editor:open
async function handleEditorReadFile(
_event: IpcMainInvokeEvent,
filePath: string // от renderer
): Promise<IpcResult<ReadFileResult>> {
return wrapHandler('readFile', async () => {
if (!activeProjectRoot) throw new Error('Editor not initialized');
// 1. Path validation (traversal, sensitive, symlink)
const validation = validateFilePath(filePath, activeProjectRoot);
if (!validation.valid) throw new Error(validation.error!);
// 2. Device path block
if (isDevicePath(validation.normalizedPath!)) throw new Error('Device files blocked');
// 3. File type check
const stats = await fs.lstat(validation.normalizedPath!);
if (!stats.isFile()) throw new Error('Not a regular file');
// 4. Size check
if (stats.size > MAX_FILE_SIZE) throw new Error('File too large');
// 5. Read
const content = await fs.readFile(validation.normalizedPath!, 'utf8');
// 6. Post-read TOCTOU verify
const realPath = await fs.realpath(validation.normalizedPath!);
const postValidation = validateFilePath(realPath, activeProjectRoot);
if (!postValidation.valid) throw new Error('Path changed during read');
return { content, size: stats.size, truncated: false, encoding: 'utf-8' };
});
}
Critical Files for Implementation
List 3-5 files most critical for implementing this plan:
/Users/belief/dev/projects/claude/claude_team/src/renderer/components/team/review/ReviewFileTree.tsx- FileTree logic to extract (buildTree algorithm, TreeNode type, collapse/expand)/Users/belief/dev/projects/claude/claude_team/src/renderer/components/team/review/CodeMirrorDiffView.tsx- CodeMirror infrastructure to extract (language detection, theme, Compartment pattern)/Users/belief/dev/projects/claude/claude_team/src/renderer/store/slices/changeReviewSlice.ts- Pattern to follow for editorSlice (fileContents cache, editedContents, saveEditedFile)/Users/belief/dev/projects/claude/claude_team/src/main/ipc/review.ts- IPC handler pattern to follow (wrapHandler, module-level state, deps injection) + EXISTING VULNERABILITY in saveEditedFile/Users/belief/dev/projects/claude/claude_team/src/main/utils/pathValidation.ts- Security validation to REUSE (not rewrite) -- validateFilePath, SENSITIVE_PATTERNS, symlink resolution
Performance-Critical Reuse Notes
Дополнение после Performance Review (plan-architecture.md секция 19). Конкретные performance-аспекты при переиспользовании кода.
CodeMirrorDiffView -- что НЕ копировать
editorViewMapRef из ChangeReviewDialog (строка 91) хранит Map<string, EditorView> для всех видимых файлов в continuous scroll view. Это допустимо для review (10-50 файлов одновременно), но НЕДОПУСТИМО для editor с 20+ табами.
Для editor использовать EditorState pooling:
// ПРАВИЛЬНО для editor:
const stateCache = useRef(new Map<string, EditorState>());
const viewRef = useRef<EditorView | null>(null);
// При переключении таба:
stateCache.current.set(oldTabId, viewRef.current!.state);
viewRef.current!.destroy();
viewRef.current = new EditorView({
state: stateCache.current.get(newTabId)!,
parent: containerRef.current!,
});
Паттерн initialState из CodeMirrorDiffView (строка 56, 699-705) -- это именно то, что нужно.
changeReviewSlice -- что НЕ копировать
editedContents: Record<string, string> (строка 74) хранит полный текст каждого редактированного файла в Zustand. В review это терпимо (изменения применяются и сбрасываются). Для editor каждый keystroke вызывает set() с новым Record -- все Zustand-подписчики перерисовываются.
Для editor контент живёт только в EditorState, не в Zustand. В store хранить:
editorModifiedFiles: Set<string> // dirty flags, не содержимое
@tanstack/react-virtual -- использовать для FileTree
Уже в проекте. Примеры:
DateGroupedSessions.tsx-- виртуализация списка сессийChatHistory.tsx-- виртуализация чатаNotificationsView.tsx-- виртуализация уведомлений
Для FileTree (итерация 4): flattenTree() -> FlatNode[] + useVirtualizer().
MembersJsonEditor -- правильный lifecycle паттерн
MembersJsonEditor.tsx (строки 27-73) -- образцовый паттерн для editor:
EditorState.create()с extensionsnew EditorView({ state, parent })-- один раз при mountview.destroy()-- в cleanup useEffect- Обновление doc через
view.dispatch({ changes: ... })-- при prop change onChangeRef.current = onChange-- для callback без re-create view
Этот паттерн масштабировать до EditorState pooling (Map вместо одного state).