# 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](https://github.com/Oleafly/Oleafly/blob/main/README.md#architecture) para ver el reparto entre frontend, Rust y sidecars, y [Desarrollo](/docs/engineering/development/) 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

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](#module-resolution)).

## 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](#the-contribution-registry)) | 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:

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

## 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:

```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);
```

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.

### Shims

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

`@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 con `when(ctx)`, hook opcional `useBadge()` (por ejemplo, el número de cambios de Git) y el componente `panel` de la barra lateral.
- **Comandos** (`registerCommand`): se ofrecen en la paleta de comandos (Cmd+K, agrupados por `group`) y/o en el omnibar (Cmd+P) mediante `surfaces`. 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:

```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"),
});
```

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

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:

1. `tsconfig.json` → `compilerOptions.paths`. Las subrutas como `@oleafly/preflight/pdf-extract` necesitan su propia entrada explícita.
2. `vite.config.ts` → `resolve.alias`. Los alias de texto coinciden por prefijo, así que las subrutas se resuelven sin otra entrada.
3. `vitest.config.ts` → el mismo `resolve.alias`, **y además** `test.include` debe cubrir `packages/**/*.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

- Las **pruebas** viven junto al código fuente (`packages/*/src/*.test.ts`) y se ejecutan en el mismo `pnpm test` que 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

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