# Архитектура фронтенда

Как Oleafly разделяет настольное приложение между рабочим пространством фронтенда, сервисами на Rust и закреплёнными сайдкарами.

Фронтенд Oleafly — это workspace на pnpm: оболочка приложения находится в
корне репозитория (`src/`), а переиспользуемый код фронтенда — в `packages/*`
в виде двенадцати узкоспециализированных пакетов. Бэкенд Tauri находится в
`src-tauri/`, а переиспользуемый код провайдеров ИИ и агента — в
`crates/oleafly-agent/`. Общую картину «фронтенд/Rust/сайдкары» см. в
[схеме архитектуры в README](https://github.com/Oleafly/Oleafly/blob/main/README.md#architecture),
а повседневные команды — в разделе [Разработка](/docs/engineering/development/).

Этот документ описывает: что представляет собой каждый пакет, правила
инверсии зависимостей, которые держат их чистыми, реестр контрибуций,
который подключает функции к оболочке, как работает разрешение модулей
и как добавить или извлечь пакет.

## Зачем нужен workspace

Раньше каждая функция обращалась напрямую к хранилищам Zustand приложения,
IPC-клиенту Tauri и UI-киту shadcn. Из-за этого функции нельзя было отделить
от оболочки: ничего нельзя было тестировать, переиспользовать или
анализировать изолированно. Workspace переворачивает это устройство.
Пакеты содержат логику функций и объявляют узкие интерфейсы, называемые
портами, для всего, что им нужно. Приложение реализует эти порты поверх
своих хранилищ и клиента Tauri и внедряет их. Приложение зависит от
пакетов, но никогда наоборот.

Пакеты используются **как исходный код TypeScript** (их `main` указывает
на `src/index.ts`). Нет шага сборки, нет публикации и нет расхождения
версий: Vite, Vitest и `tsc` разрешают `@oleafly/*` напрямую в исходники
пакетов через алиасы (см. [Разрешение модулей](#module-resolution)).

## Пакеты

| Пакет | Что это | Внедряемые порты | Заметные зависимости |
| --- | --- | --- | --- |
| `@oleafly/latex` | Чистая логика LaTeX/рисунков: модель диаграмм, сериализатор TikZ (`modelToTikz`, двустороннее преобразование встроенной модели), сборщик автономного документа (`buildStandaloneDoc`), вспомогательные функции для имён и байтов рисунков | нет (чистый) | нет |
| `@oleafly/ai-core` | Отображаемые метаданные провайдеров и выбор моделей на фронтенде, в синхронизации с каталогом на Rust | нет (чистый) | нет |
| `@oleafly/latex-intelligence` | Проверенные типы и доступ к данным корпуса LaTeX, используемого интеллектом редактора | нет (чистый) | нет |
| `@oleafly/registry` | Реестр контрибуций: вкладки рельса, команды палитры/омнибара, наборы инструментов ИИ ([подробности ниже](#the-contribution-registry)) | нет (контрибуции несут собственное поведение) | react (типы) |
| `@oleafly/preflight` | Механизм предпроверки: типизация документа, правила для исходного текста, PDF и библиографии, разбор ATS, подготовка доступного экспорта, подсчёт баллов. `pdf-extract` поставляется как подпуть `@oleafly/preflight/pdf-extract`, чтобы pdf.js не попадал в графы node-тестов | нет (чистый) | `pdfjs-dist` (только подпуть) |
| `@oleafly/pdf-to-latex` | Локальный конвейер восстановления LaTeX из PDF: группировка строк, определение заголовков и колонок, вывод стилей, сопоставление символов, извлечение рисунков, экранирование и сборка документа. Адаптер pdf.js поставляется как подпуть `@oleafly/pdf-to-latex/pdf-adapter` по той же причине графа node-тестов | нет (чистый) | `pdfjs-dist` (только подпуть) |
| `@oleafly/wysiwyg` | Ядро ограниченного визуального редактора: общая схема, разбор и сериализация LaTeX и Markdown, а также «сырые» строчные/блочные узлы, которые сохраняют неподдерживаемый исходный текст видимым вместо того, чтобы отбрасывать его | нет (чистый) | нет |
| `@oleafly/editor` | Ядро CodeMirror для LaTeX, Typst и Markdown: `CodeMirrorEditor`, контроллер представления редактора (`insertAtCursor`, `gotoLine`, …), языки и автодополнения, тема, сворачивание кода, закреплённые заголовки, линтеры, latex-mask, предпросмотр формул (KaTeX для LaTeX, Typst для Typst), вспомогательные средства редактирования Typst и преобразование формул из LaTeX в Typst, встроенный визуальный режим для LaTeX и Typst (`@oleafly/editor/visual`), панель поиска, линтеры орфографии и грамматики | `EditorHost` (модель документа + настройки, в виде хуков), `SpellHost` (проверка орфографии/Harper/словарь), `setBibKeysProvider`, `extraExtensions`/`extraKeymap` | `@codemirror/*`, `@replit/codemirror-vim`, `@replit/codemirror-emacs`, `katex` |
| `@oleafly/preview` | Виртуализированный просмотрщик pdf.js (`PdfViewer`), контроллер страниц SyncTeX (`gotoRect`, `pageClickToBp`), воркер pdf.js и полифиллы WebView (`@oleafly/preview/polyfills`, импортируется первым в `main.tsx`) | проп `onOpenLink` (ссылки в системном браузере), `setPdfLogger` | `pdfjs-dist` |
| `@oleafly/diagram` | Визуальный редактор диаграмм для TikZ, Typst (fletcher) и Mermaid: холст React Flow, инспектор фигур, вкладки «Рисование»/«Код», поток «компиляция — предпросмотр — вставка». Для каждого языка в `src/languages/` есть писатель, читатель, а также его формы для вставки и файлов, и холст с кодом синхронизируются в обе стороны | `DiagramHost` (компиляция LaTeX, отрисовка Typst и Mermaid, файловый ввод-вывод, вставка в редактор, исправление с помощью ИИ), контекст `DiagramKit` (Button/Tooltip/Select/toast/theme) | `@xyflow/react`, `@codemirror/*`, `@oleafly/latex`, `@oleafly/editor` |
| `@oleafly/ai-tools` | Определения инструментов для проекта, исследования, памяти и рисунков с мутациями, учитывающими подтверждение | `AiToolsHost` (файлы, компиляция, индекс символов, конвейер рисунков, редактор) | `@oleafly/latex` |
| `@oleafly/templates` | Галерея шаблонов новых проектов (двухшаговый мастер, категории, предпросмотры, однократные загрузки ресурсов с индикатором прогресса) | `TemplatesHost` (предпросмотры, загрузка ресурсов, журналирование), `TemplatesKit` (Button/Tooltip), варианты цветов как пропсы | нет, кроме UI-утилит |

Направление зависимостей, строго соблюдаемое:

Пакет никогда не должен импортировать из `src/` (`@/…`), хранилища Zustand,
`@tauri-apps/*` или UI-компонентов приложения. Проверка чистоты — часть
гигиены извлечения:

```bash
grep -rn 'from "@/\|@tauri\|zustand' packages/*/src   # must return nothing
```

## Паттерн портов

Пакеты объявляют интерфейсы. Приложение реализует их один раз, обычно
в виде объекта-адаптера на уровне модуля, замыкающего хранилища. Четыре
повторяющихся формы:

**Порты-хосты**: один `interface XHost` на пакет, покрывающий нужные ему
сервисы. Приложение собирает адаптер с императивными чтениями хранилищ:

```ts
// src/lib/ai-tools.ts (app adapter, abridged)
const HOST: AiToolsHost = {
  getProjectId: () => useFilesStore.getState().projectId,
  readFileContent,                       // Tauri client passthrough
  recompile: () => useCompileStore.getState().recompile(),
  insertAtCursor,                        // editor controller
  // ...
};
export const createOleaflyTools = (opts?) => createOleaflyToolsCore(HOST, opts);
```

Хранение забот, относящихся только к приложению, внутри адаптера также
не пускает зависимости в пакеты. Учётные данные и запросы провайдеров
остаются в Rust, поэтому пакеты фронтенда получают только то состояние
и те результаты, которые требуются их интерфейсам.

**UI-киты**: React-контекст (или проп) структурно типизированных
компонентов: `Button: ComponentType<{ variant?: …; onClick?: () => void }>`.
Приложение передаёт свои компоненты shadcn без изменений, поскольку
меньший структурный тип совместим. Киты также несут `toast` и хук
`useThemeMode()`.

**Порты в виде хуков**: для пакетов, которым нужно *подписываться* на
состояние приложения. `EditorHost` редактора имеет члены-хуки
(`useActivePath()`, `useSettings()`) плюс императивные двойники
(`getActivePath()`) для слушателей событий. Правила: объект хоста должен
находиться на уровне модуля (стабильная идентичность), а его члены `use*`
должны вызывать хуки безусловно.

**Модульные синглтоны**: для сквозного связующего кода;
`setPdfLogger(fn)`, `setSpellHost(host)`, `setBibKeysProvider(fn)` имеют
пустые умолчания и устанавливаются шимом приложения как побочный эффект
импорта.

### Шимы

Каждое извлечение оставляло тонкий файл по старому пути `@/…`, который
реэкспортирует (или оборачивает) пакет, поэтому потребители и существующие
пути `vi.mock(...)` не менялись. Примеры:
`src/components/editor/cm/controller.ts` (реэкспорт),
`src/components/pdf/PdfViewer.tsx` (обёртка, внедряющая открыватель ссылок
плагина оболочки), `src/lib/ai-tools.ts` (адаптер + исходные сигнатуры
фабрики). Новый код может импортировать `@oleafly/*` напрямую. Шимы
существуют для совместимости, а не как предпочтительный путь.

## Реестр контрибуций

`@oleafly/registry` — способ подключать функции к оболочке приложения так,
чтобы оболочка о них не знала. Три вида контрибуций:

- **Вкладки рельса** (`registerRailTab`): значок, секция (`explore` /
  `review` / `assist`, разделители рисуются между непустыми секциями),
  необязательная видимость `when(ctx)`, необязательный хук `useBadge()`
  (например, количество изменений Git) и компонент боковой панели `panel`.
- **Команды** (`registerCommand`): подаются в командную палитру
  (Cmd+K, группируются по `group`) и/или в омнибар
  (Cmd+P) через `surfaces`. Метки и значки могут быть
  функциями контекста (например, «Переключиться на светлую тему»).
- **Наборы инструментов ИИ** (`registerAiToolset`): режим чата
  (`"chat"`, `"figure"`), сопоставленный с фабрикой набора инструментов.
  Панель чата ищет набор инструментов активного режима вместо жёсткого
  закрепления одного.

`AppContext` (`{ projectId, projectKind, theme }`) строится каждой
отображающей поверхностью из собственных подписок, поэтому условия
`when`/`label` остаются реактивными.

Регистрация **статическая**: `src/contributions/` регистрирует все
встроенные элементы (вкладки, команды, наборы инструментов), а `main.tsx`
вызывает `registerContributions()` один раз до монтирования оболочки.
`Rail`, `Sidebar`, `CommandPalette`, `SearchOmnibar` и `ChatPanel`
отображают только то, что зарегистрировано.

Чтобы добавить вкладку или команду функции, зарегистрируйте её. Правки
оболочки не нужны:

```ts
// src/contributions/my-feature.tsx (called from src/contributions/index.ts)
registerRailTab({
  id: "review", label: "Review", icon: MessageSquare,
  section: "review", order: 55,
  when: (ctx) => ctx.projectKind !== "image",
  panel: ReviewPanel,
});
registerCommand({
  id: "omnibar.review", surfaces: ["omnibar"], order: 45,
  label: "Open review", keywords: "review comments",
  run: () => useSettingsStore.getState().setRailTab("review"),
});
```

Это внутренний API, а не SDK плагинов: формы могут свободно меняться,
и ничего не загружается динамически. Отложено до появления реального
потребителя: форматы меню экспорта, схемы настроек и поведения в
зависимости от типа проекта как контрибуции.

## Разрешение модулей

Поскольку пакеты используются как исходный код, один и тот же алиас
объявляется в трёх местах, и все три должны оставаться синхронными при
добавлении пакета:

1. `tsconfig.json` → `compilerOptions.paths`. Подпути вроде
   `@oleafly/preflight/pdf-extract` требуют собственной явной записи.
2. `vite.config.ts` → `resolve.alias`. Строковые алиасы сопоставляются
   по префиксу, поэтому подпути разрешаются без дополнительной записи.
3. `vitest.config.ts` → тот же `resolve.alias`, **плюс** `test.include`
   должен охватывать `packages/**/*.test.ts`. Иначе перенесённые тесты
   молча выпадают из запуска.

Кроме того: корневой `package.json` зависит от каждого пакета через
`"workspace:*"`, а в `src/styles/globals.css` есть
`@source "../../packages"`, чтобы Tailwind v4 сканировал исходники пакетов
в поисках имён классов.

Импорты, работающие только в Vite (`?worker&url`-воркеры, импорты CSS),
работают из исходников пакетов без изменений, поскольку алиасы указывают
на исходный код. Не помещайте в barrel-файлы, которые импортируют тесты
в окружении node, ничего, что тянет за собой pdf.js: именно поэтому
`pdf-extract` и просмотрщик пакета preview достижимы только там, где
существует DOM.

## Работа с пакетами

- **Тесты** лежат рядом с исходным кодом (`packages/*/src/*.test.ts`)
  и запускаются в том же `pnpm test`, что и тесты приложения. Следите
  за *количеством* тестов при перемещении файлов.
- **Зависимости** объявляются свободно (`"react": "*"`). pnpm
  дедуплицирует их относительно корня, где находятся настоящие нижние
  границы версий. Не объявляйте зависимости, которые пакет не импортирует.
- **Гейты для любого изменения пакета**: `pnpm exec tsc --noEmit`,
  `pnpm build`, `pnpm test` (неизменное количество) и приведённая выше
  проверка чистоты через grep.
- **Извлечение нового**: перемещайте файлы через `git mv` (история
  сохраняется), определите порты, оставьте шимы по старым путям,
  подключите три алиаса, затем запустите гейты. Не извлекайте функцию,
  пока активно её дорабатываете.
- **Лицензирование**: каждый пакет — `AGPL-3.0-or-later`, как и приложение.

## Что остаётся в приложении

В `src/` остаётся всё, что действительно является приложением фронтенда:
хранилища Zustand, IPC-клиент Tauri (`src/lib/tauri.ts`), UI-кит shadcn,
компоненты макета и оболочки, адаптеры портов и шимы, `src/contributions/`
и связующий код функций, по своей природе привязанный к хранилищам
(UI панели предпроверки, плагин встроенного редактирования с ИИ, а также
интеллект кода и наведения, внедряемый в редактор как `extraExtensions`).
Команды Tauri и интеграция с операционной системой остаются в `src-tauri/`.
Клиенты провайдеров, потоковая передача и оркестрация агента принадлежат
`crates/oleafly-agent/`, чтобы секреты и запросы к провайдерам оставались
вне webview.
