Перейти к содержимому
Oleaflyv0.4.7

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

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

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

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

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

ПакетЧто этоВнедряемые портыЗаметные зависимости
@oleafly/latexЧистая логика LaTeX/рисунков: модель диаграмм, сериализатор TikZ (modelToTikz, двустороннее преобразование встроенной модели), сборщик автономного документа (buildStandaloneDoc), вспомогательные функции для имён и байтов рисунковнет (чистый)нет
@oleafly/ai-coreОтображаемые метаданные провайдеров и выбор моделей на фронтенде, в синхронизации с каталогом на Rustнет (чистый)нет
@oleafly/latex-intelligenceПроверенные типы и доступ к данным корпуса LaTeX, используемого интеллектом редакторанет (чистый)нет
@oleafly/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 (ссылки в системном браузере), setPdfLoggerpdfjs-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 и адаптеры портов. Адаптеры реализуют порты, которые объявляют функциональные пакеты @oleafly, например EditorHost, и приложение внедряет их. Функциональные пакеты могут зависеть от чистых листовых пакетов вроде latex, ai-core и registry. Клиент Tauri общается с бэкендом на Rust через IPC Tauri. Пакеты никогда не импортируют приложение, его хранилища или API Tauri.Оболочка приложения, src/Пакеты @oleafly/*ХранилищаZustandАдаптерыРеализуют портыНабор интерфейсаshadcnКлиент Taurisrc/lib/tauri.tsФункциональные пакетыОбъявляют порты, например EditorHostЛистовые пакетыlatex, ai-core, registryБэкенд на Rustsrc-tauri, crates/oleafly-agentВнедряетTauri IPCПакеты никогда не импортируют приложение, его хранилища или API Tauri.Как оболочка приложения и пакеты зависят друг от другаОболочка приложения в src/ содержит хранилища Zustand, набор интерфейсных компонентов, клиент Tauri и адаптеры портов. Адаптеры реализуют порты, которые объявляют функциональные пакеты @oleafly, например EditorHost, и приложение внедряет их. Функциональные пакеты могут зависеть от чистых листовых пакетов вроде latex, ai-core и registry. Клиент Tauri общается с бэкендом на Rust через IPC Tauri. Пакеты никогда не импортируют приложение, его хранилища или API Tauri.Оболочка приложения, src/Пакеты @oleafly/*ХранилищаНабор интерфейсаКлиент TauriАдаптерыБэкенд на Rustsrc-tauri, cratesФункциональные пакетыОбъявляют портыЛистовые пакетыlatex, ai-core, registryTauri IPCВнедряетПакеты никогда не импортируют приложение,его хранилища или API Tauri.
Приложение зависит от пакетов и внедряет адаптеры для объявленных ими портов. Пакеты никогда не импортируют приложение.

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

Окно терминала
grep -rn 'from "@/\|@tauri\|zustand' packages/*/src # must return nothing

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

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

// 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 отображают только то, что зарегистрировано.

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

// 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.

Просмотр изображения

Не удалось загрузить ответы ИИ

Попробуйте снова или воспользуйтесь поиском по документации.