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

42 KiB
Raw Blame History

Анализ переиспользования кодовой базы для 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. Структура:

  1. Выделить buildTree() и TreeNode в утилиту src/renderer/utils/fileTreeBuilder.ts
  2. Создать generic FileTree компонент с renderItem callback (render-prop для кастомизации правой части каждого файлового элемента)
  3. ReviewFileTree становится тонкой обёрткой вокруг FileTree с review-специфичным renderItem
  4. EditorFileTree -- вторая обёртка для редактора (показывает иконки по типу файла, 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:

  1. Извлечь getLanguageExtension() в src/renderer/utils/codemirrorLanguages.ts
  2. Извлечь базовую тему в src/renderer/utils/codemirrorTheme.ts
  3. Новый 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 workflow
  • ViewedProgressBar -- review-only
  • ConflictDialog -- 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 каналы:

  1. editor:listDirectory(dirPath) -- рекурсивный listing файлов (с ignore-паттернами: .git, node_modules, etc.)
  2. editor:readFile(filePath) -- чтение файла без ограничений maxTokens (в отличие от read-mentioned-file)
  3. editor:saveFile(filePath, content) -- можно переиспользовать review:saveEditedFile, но лучше отдельный канал с более широкой валидацией
  4. editor:createFile(filePath, content?) -- создание нового файла
  5. editor:deleteFile(filePath) -- удаление файла (с confirm на renderer стороне)
  6. editor:renameFile(oldPath, newPath) -- переименование
  7. 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().

Куда добавить:

  1. src/renderer/store/slices/editorSlice.ts -- новый slice
  2. Добавить EditorSlice в AppState type в types.ts
  3. Добавить ...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-file
  • editedContents: 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. Причины:

  1. Уже 20+ пакетов CodeMirror в зависимостях -- нулевой overhead по bundle size
  2. Работающая инфраструктура: getSyncLanguageExtension(), getAsyncLanguageDesc(), тема, Compartment-паттерн -- всё протестировано в production
  3. @codemirror/autocomplete уже установлен -- автодополнение из коробки
  4. CodeMirror = код-редактор, ProseMirror = rich text / WYSIWYG. Для проектного редактора нужен именно код-редактор
  5. 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 -- конфликт.

Рекомендация:

  • mtime check перед записью (как 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. Итоговая архитектурная рекомендация

Что ИЗВЛЕЧЬ из существующего кода (рефакторинг):

  1. buildTree() + TreeNode --> src/renderer/utils/fileTreeBuilder.ts
  2. getSyncLanguageExtension() + getAsyncLanguageDesc() --> src/renderer/utils/codemirrorLanguages.ts
  3. Базовая CM тема (без diff) --> src/renderer/utils/codemirrorTheme.ts
  4. ReviewFileTree --> generic FileTree + ReviewFileTree wrapper

Что СОЗДАТЬ с нуля:

  1. src/renderer/store/slices/editorSlice.ts
  2. src/main/ipc/editor.ts + handler'ы
  3. src/preload/constants/ipcChannels.ts -- добавить EDITOR_* каналы
  4. src/preload/index.ts -- добавить editor API
  5. src/renderer/components/editor/ -- компоненты редактора
  6. 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-specific
  • ReviewFileTree -- 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:

  1. EditorState.create() с extensions
  2. new EditorView({ state, parent }) -- один раз при mount
  3. view.destroy() -- в cleanup useEffect
  4. Обновление doc через view.dispatch({ changes: ... }) -- при prop change
  5. onChangeRef.current = onChange -- для callback без re-create view

Этот паттерн масштабировать до EditorState pooling (Map вместо одного state).