Arquitectura del frontend
Cómo reparte Oleafly la aplicación de escritorio entre el espacio de trabajo del frontend, los servicios en Rust y los sidecars con versión fija.
El frontend de Oleafly es un espacio de trabajo de pnpm: el shell de la aplicación vive en la raíz del repositorio (src/) y el código de frontend reutilizable vive en packages/* como doce paquetes con un propósito concreto. El backend de Tauri vive en src-tauri/, mientras que el código reutilizable de proveedores de IA y agentes vive en crates/oleafly-agent/. Consulta el diagrama de arquitectura del README (se abre en una pestaña nueva) para ver el reparto entre frontend, Rust y sidecars, y Desarrollo para los comandos del día a día.
Este documento explica qué es cada paquete, las reglas de inversión de dependencias que los mantienen puros, el registro de contribuciones que conecta las funciones con el shell, cómo funciona la resolución de módulos y cómo añadir o extraer un paquete.
Por qué un espacio de trabajo
Sección titulada «Por qué un espacio de trabajo»Antes, cada función accedía directamente a los stores de Zustand de la aplicación, al cliente IPC de Tauri y al kit de interfaz de shadcn. Eso hacía que las funciones no se pudieran separar del shell: nada se podía probar, reutilizar ni analizar de forma aislada. El espacio de trabajo invierte esa relación. Los paquetes contienen la lógica de cada función y declaran interfaces estrechas, llamadas puertos, para todo lo que necesitan. La aplicación implementa esos puertos sobre sus stores y su cliente de Tauri, y los inyecta. La aplicación depende de los paquetes, nunca al revés.
Los paquetes se consumen como código fuente TypeScript (su main apunta a src/index.ts). No hay paso de compilación, ni publicación, ni desajustes de versión: Vite, Vitest y tsc resuelven @oleafly/* directamente al código fuente de los paquetes mediante alias (consulta Resolución de módulos).
Los paquetes
Sección titulada «Los paquetes»| Paquete | Qué es | Puertos inyectados | Dependencias destacadas |
|---|---|---|---|
@oleafly/latex | Lógica pura de LaTeX y figuras: el modelo de diagramas, el serializador TikZ (modelToTikz, ida y vuelta del modelo incrustado), el constructor de documentos independientes (buildStandaloneDoc), ayudantes de nombres y bytes de figuras | ninguno (puro) | ninguna |
@oleafly/ai-core | Metadatos de presentación de proveedores y selección de modelos en el frontend, alineados con el catálogo de Rust | ninguno (puro) | ninguna |
@oleafly/latex-intelligence | Tipos validados y acceso a datos del corpus de LaTeX que usa la inteligencia del editor | ninguno (puro) | ninguna |
@oleafly/registry | El registro de contribuciones: pestañas de la barra lateral, comandos de la paleta y del omnibar, conjuntos de herramientas de IA (detalles más abajo) | ninguno (las contribuciones llevan su propio comportamiento) | react (tipos) |
@oleafly/preflight | El motor de preflight: tipado del documento, reglas de fuente, PDF y referencias, análisis ATS, preparación de exportación accesible y puntuación. pdf-extract se distribuye como la subruta @oleafly/preflight/pdf-extract para que pdf.js quede fuera de los grafos de pruebas de node | ninguno (puro) | pdfjs-dist (solo la subruta) |
@oleafly/pdf-to-latex | La canalización local de reconstrucción de PDF a LaTeX: agrupación de líneas, detección de encabezados y columnas, inferencia de estilos, correspondencia de símbolos, extracción de figuras, escapado y montaje del documento. El adaptador de pdf.js se distribuye como la subruta @oleafly/pdf-to-latex/pdf-adapter por el mismo motivo del grafo de pruebas de node | ninguno (puro) | pdfjs-dist (solo la subruta) |
@oleafly/wysiwyg | El núcleo del editor visual acotado: esquema compartido, análisis y serialización de LaTeX y Markdown, y los nodos en línea y de bloque sin procesar que mantienen visible el código no compatible en lugar de descartarlo | ninguno (puro) | ninguna |
@oleafly/editor | El núcleo de CodeMirror para LaTeX, Typst y Markdown: CodeMirrorEditor, el controlador de la vista del editor (insertAtCursor, gotoLine, …), lenguajes y completado, tema, plegado, encabezados fijos, linters, latex-mask, vista previa de matemáticas (KaTeX para LaTeX, Typst para Typst), ayudas de edición de Typst y conversión de matemáticas de LaTeX a Typst, el modo Visual en el sitio para LaTeX y Typst (@oleafly/editor/visual), panel de búsqueda, linters de ortografía y gramática | EditorHost (modelo del documento y ajustes, con forma de hook), SpellHost (corrector ortográfico, Harper, diccionario), setBibKeysProvider, extraExtensions/extraKeymap | @codemirror/*, @replit/codemirror-vim, @replit/codemirror-emacs, katex |
@oleafly/preview | El visor de pdf.js virtualizado (PdfViewer), el controlador de páginas de SyncTeX (gotoRect, pageClickToBp), el worker de pdf.js y los polyfills de WebView (@oleafly/preview/polyfills, importados primero en main.tsx) | prop onOpenLink (enlaces del navegador del sistema), setPdfLogger | pdfjs-dist |
@oleafly/diagram | El compositor visual de diagramas para TikZ, Typst (fletcher) y Mermaid: lienzo de React Flow, inspector de formas, pestañas Dibujo/Código y flujo de compilar, previsualizar e insertar. Cada lenguaje en src/languages/ tiene un escritor, un lector y sus formas de inserción y de archivo, y el lienzo y el código se mantienen sincronizados en ambos sentidos | DiagramHost (compilación de LaTeX, renderizado de Typst y Mermaid, E/S de archivos, inserción en el editor, corrección con IA), contexto DiagramKit (Button/Tooltip/Select/toast/tema) | @xyflow/react, @codemirror/*, @oleafly/latex, @oleafly/editor |
@oleafly/ai-tools | Definiciones de herramientas de proyecto, investigación, memoria y figuras, con mutaciones que respetan las aprobaciones | AiToolsHost (archivos, compilación, índice de símbolos, canalización de figuras, editor) | @oleafly/latex |
@oleafly/templates | La galería de plantillas para proyectos nuevos (asistente en dos pasos, categorías, vistas previas, descargas puntuales de recursos con progreso) | TemplatesHost (vistas previas, descargas de recursos, registro), TemplatesKit (Button/Tooltip), opciones de color como props | nada más allá de utilidades de interfaz |
Dirección de las dependencias, aplicada de forma estricta:
Un paquete nunca debe importar desde src/ (@/…), un store de Zustand, @tauri-apps/* ni los componentes de interfaz de la aplicación. La comprobación de pureza forma parte de la higiene de extracción:
grep -rn 'from "@/\|@tauri\|zustand' packages/*/src # must return nothingEl patrón de puertos
Sección titulada «El patrón de puertos»Los paquetes declaran interfaces. La aplicación las implementa una vez, normalmente como un objeto adaptador a nivel de módulo que encapsula sus stores. Hay cuatro formas recurrentes:
Puertos host: una interface XHost por paquete que cubre los servicios que necesita. La aplicación construye el adaptador con lecturas imperativas de los stores:
// 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);Mantener dentro del adaptador lo que solo concierne a la aplicación también mantiene las dependencias fuera de los paquetes. Las credenciales y las peticiones de los proveedores se quedan en Rust, así que los paquetes de frontend solo reciben el estado y los resultados que exigen sus interfaces.
Kits de interfaz: un contexto de React (o una prop) con componentes de tipo estructural: Button: ComponentType<{ variant?: …; onClick?: () => void }>. La aplicación pasa sus componentes de shadcn sin cambios porque el tipo estructural más pequeño es compatible. Los kits también incluyen toast y un hook useThemeMode().
Puertos con forma de hook: para paquetes que deben suscribirse al estado de la aplicación. El EditorHost del editor tiene miembros hook (useActivePath(), useSettings()) y gemelos imperativos (getActivePath()) para los listeners de eventos. Reglas: el objeto host debe estar a nivel de módulo (identidad estable) y sus miembros use* deben llamar a los hooks de forma incondicional.
Singletons de módulo: para el pegamento transversal, setPdfLogger(fn), setSpellHost(host) y setBibKeysProvider(fn) tienen valores por defecto que no hacen nada y el shim de la aplicación los instala como efecto secundario de la importación.
Cada extracción dejó un archivo fino en la antigua ruta @/… que reexporta (o envuelve) el paquete, así que los consumidores y las rutas existentes de vi.mock(...) nunca tuvieron que cambiar. Ejemplos: src/components/editor/cm/controller.ts (reexportación), src/components/pdf/PdfViewer.tsx (envoltorio que inyecta el abridor de enlaces del plugin shell) y src/lib/ai-tools.ts (adaptador y firmas originales de las fábricas). El código nuevo puede importar @oleafly/* directamente. Los shims existen por compatibilidad, no como la vía preferida.
El registro de contribuciones
Sección titulada «El registro de contribuciones»@oleafly/registry es la forma en que las funciones se conectan al shell de la aplicación sin que el shell las conozca. Hay tres tipos de contribución:
- Pestañas de la barra lateral (
registerRailTab): icono, sección (explore/review/assist, con separadores entre las secciones que no están vacías), visibilidad opcional conwhen(ctx), hook opcionaluseBadge()(por ejemplo, el número de cambios de Git) y el componentepanelde la barra lateral. - Comandos (
registerCommand): se ofrecen en la paleta de comandos (Cmd+K, agrupados porgroup) y/o en el omnibar (Cmd+P) mediantesurfaces. Las etiquetas y los iconos pueden ser funciones del contexto (por ejemplo, “Cambiar al tema claro”). - Conjuntos de herramientas de IA (
registerAiToolset): un modo de chat ("chat","figure") asociado a una fábrica de herramientas. El panel de chat busca las herramientas del modo activo en lugar de tener unas fijas en el código.
Cada superficie que renderiza construye AppContext ({ projectId, projectKind, theme }) a partir de sus propias suscripciones, así que las condiciones when/label siguen siendo reactivas.
El registro es estático: src/contributions/ registra todo lo incorporado (pestañas, comandos, conjuntos de herramientas) y main.tsx llama una vez a registerContributions() antes de montar el shell. Rail, Sidebar, CommandPalette, SearchOmnibar y ChatPanel solo renderizan lo que está registrado.
Para añadir la pestaña o el comando de una función, regístralo. Sin tocar el shell:
// 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"),});Es una API interna, no un SDK de plugins: sus formas pueden cambiar libremente y nada se carga de forma dinámica. Quedan pendientes hasta que exista un consumidor real: formatos del menú de exportación, esquemas de ajustes y comportamientos por tipo de proyecto como contribuciones.
Resolución de módulos
Sección titulada «Resolución de módulos»Como los paquetes se consumen como código fuente, el mismo alias se declara en tres sitios, y los tres deben mantenerse sincronizados cuando se añade un paquete:
tsconfig.json→compilerOptions.paths. Las subrutas como@oleafly/preflight/pdf-extractnecesitan su propia entrada explícita.vite.config.ts→resolve.alias. Los alias de texto coinciden por prefijo, así que las subrutas se resuelven sin otra entrada.vitest.config.ts→ el mismoresolve.alias, y ademástest.includedebe cubrirpackages/**/*.test.ts. Si no, las pruebas movidas desaparecen de la ejecución sin avisar.
Además: el package.json raíz depende de cada paquete mediante "workspace:*", y src/styles/globals.css tiene @source "../../packages" para que Tailwind v4 busque nombres de clases en el código fuente de los paquetes.
Las importaciones exclusivas de Vite (workers con ?worker&url, importaciones de CSS) funcionan sin cambios desde el código fuente de los paquetes porque los alias apuntan al código fuente. Mantén todo lo que arrastre pdf.js fuera de los barrels que importan las pruebas en entorno node: por eso pdf-extract y el visor del paquete de vista previa solo son accesibles donde existe el DOM.
Trabajar en los paquetes
Sección titulada «Trabajar en los paquetes»- Las pruebas viven junto al código fuente (
packages/*/src/*.test.ts) y se ejecutan en el mismopnpm testque las pruebas de la aplicación. Vigila el número de pruebas cuando muevas archivos. - Las dependencias se declaran de forma laxa (
"react": "*"). pnpm las deduplica contra la raíz, donde están los mínimos de versión reales. No declares dependencias que el paquete no importa. - Comprobaciones para cualquier cambio en un paquete:
pnpm exec tsc --noEmit,pnpm build,pnpm test(con el mismo número de pruebas) y el grep de pureza de arriba. - Extraer algo nuevo: mueve los archivos con
git mv(el historial los sigue), define los puertos, deja shims en las rutas antiguas, conecta los tres alias y ejecuta las comprobaciones. No extraigas una función mientras sigues iterando sobre ella. - Licencia: todos los paquetes son
AGPL-3.0-or-later, igual que la aplicación.
Qué se queda en la aplicación
Sección titulada «Qué se queda en la aplicación»src/ conserva todo lo que es realmente la aplicación de frontend: los stores de Zustand, el cliente IPC de Tauri (src/lib/tauri.ts), el kit de interfaz de shadcn, los componentes de diseño y del shell, los adaptadores de puertos y los shims, src/contributions/, y el pegamento de funciones que por naturaleza está acoplado a los stores (la interfaz del panel de preflight, el plugin de edición con IA en línea y la inteligencia de código o de hover inyectada en el editor como extraExtensions). Los comandos de Tauri y la integración con el sistema operativo se quedan en src-tauri/. Los clientes de proveedores, el streaming y la orquestación de agentes pertenecen a crates/oleafly-agent/, para que los secretos y las peticiones a los proveedores queden fuera del webview.