# Frontend-Architektur

Wie Oleafly die Desktop-App zwischen Frontend-Workspace, Rust-Diensten und gepinnten Sidecars aufteilt.

Oleaflys Frontend ist ein pnpm-Workspace: Die App-Shell liegt im Wurzelverzeichnis des Repositorys (`src/`), wiederverwendbarer Frontend-Code liegt als zwölf fokussierte Pakete in `packages/*`. Das Tauri-Backend liegt in `src-tauri/`, wiederverwendbarer KI-Provider- und Agent-Code liegt in `crates/oleafly-agent/`. Das [Architekturdiagramm im README](https://github.com/Oleafly/Oleafly/blob/main/README.md#architecture) zeigt das Bild aus Frontend/Rust/Sidecar; unter [Entwicklung](/docs/engineering/development/) finden Sie die Befehle für den Alltag.

Dieses Dokument behandelt: Was jedes Paket ist, die Regeln der Abhängigkeitsinversion, die die Pakete rein halten, die Contribution-Registry, die Features mit der Shell verdrahtet, wie die Modulauflösung funktioniert und wie man ein Paket hinzufügt oder extrahiert.

## Warum ein Workspace

Früher griff jedes Feature direkt auf die Zustand-Stores der App, den Tauri-IPC-Client und das shadcn-UI-Kit zu. Damit waren Features von der Shell unzertrennlich: Nichts ließ sich isoliert testen, wiederverwenden oder durchdenken. Der Workspace kehrt das um. Die Pakete halten die Feature-Logik und deklarieren schmale Schnittstellen, sogenannte Ports, für alles, was sie brauchen. Die App implementiert diese Ports über ihre Stores und den Tauri-Client und injiziert sie. Die App hängt von den Paketen ab, niemals umgekehrt.

Pakete werden **als TypeScript-Quelltext** konsumiert (ihr `main` zeigt auf `src/index.ts`). Es gibt keinen Build-Schritt, kein Publishing und keinen Versionsdrift: Vite, Vitest und `tsc` lösen `@oleafly/*` per Aliase direkt zu den Paketquellen auf (siehe [Modulauflösung](#module-resolution)).

## Die Pakete

| Paket | Was es ist | Injizierte Ports | Wichtige Abhängigkeiten |
| --- | --- | --- | --- |
| `@oleafly/latex` | Reine Logik für LaTeX und Abbildungen: das Diagrammmodell, der TikZ-Serialisierer (`modelToTikz`, Round-Trip des eingebetteten Modells), der Builder für Standalone-Dokumente (`buildStandaloneDoc`), Helfer für Abbildungsnamen und -bytes | keine (rein) | keine |
| `@oleafly/ai-core` | Anzeigemetadaten der Provider und die Modellauswahl im Frontend, auf dem Stand des Rust-Katalogs gehalten | keine (rein) | keine |
| `@oleafly/latex-intelligence` | Validierte Typen und Datenzugriff für das LaTeX-Korpus, das die Editor-Intelligenz nutzt | keine (rein) | keine |
| `@oleafly/registry` | Die Contribution-Registry: Rail-Tabs, Palette-/Omnibar-Befehle, KI-Toolsets ([Details unten](#the-contribution-registry)) | keine (Contributions bringen ihr eigenes Verhalten mit) | react (Typen) |
| `@oleafly/preflight` | Die Preflight-Engine: Dokumenttypisierung, Regeln für Quelle/PDF/Referenzen, ATS-Parsing, Vorbereitung barrierefreier Exporte, Scoring. `pdf-extract` wird als Subpath `@oleafly/preflight/pdf-extract` ausgeliefert, damit pdf.js aus den Node-Testgraphen herausbleibt | keine (rein) | `pdfjs-dist` (nur Subpath) |
| `@oleafly/pdf-to-latex` | Die lokale Rekonstruktions-Pipeline von PDF zu LaTeX: Zeilengruppierung, Erkennung von Überschriften und Spalten, Stilinferenz, Symbol-Mapping, Abbildungsextraktion, Escaping und Dokumentzusammenbau. Der pdf.js-Adapter wird aus demselben Grund (Node-Testgraph) als Subpath `@oleafly/pdf-to-latex/pdf-adapter` ausgeliefert | keine (rein) | `pdfjs-dist` (nur Subpath) |
| `@oleafly/wysiwyg` | Der Kern des beschränkten visuellen Editors: gemeinsames Schema, Parsen und Serialisieren von LaTeX und Markdown sowie die rohen Inline-/Block-Knoten, die nicht unterstützten Quelltext sichtbar halten, statt ihn zu verwerfen | keine (rein) | keine |
| `@oleafly/editor` | Der CodeMirror-Kern für LaTeX, Typst und Markdown: `CodeMirrorEditor`, der Controller der Editor-Ansicht (`insertAtCursor`, `gotoLine`, …), Sprachen und Vervollständigungen, Theme, Folding, Sticky-Überschriften, Linter, latex-mask, Mathe-Vorschau (KaTeX für LaTeX, Typst für Typst), Typst-Bearbeitungshilfen und die Umwandlung von LaTeX-Mathe nach Typst, der In-Place-Visual-Modus für LaTeX und Typst (`@oleafly/editor/visual`), Suchpanel, Rechtschreib-/Grammatik-Linter | `EditorHost` (Dokumentmodell + Einstellungen, hook-förmig), `SpellHost` (Rechtschreibprüfung/Harper/Wörterbuch), `setBibKeysProvider`, `extraExtensions`/`extraKeymap` | `@codemirror/*`, `@replit/codemirror-vim`, `@replit/codemirror-emacs`, `katex` |
| `@oleafly/preview` | Der virtualisierte pdf.js-Viewer (`PdfViewer`), der SyncTeX-Seiten-Controller (`gotoRect`, `pageClickToBp`), der pdf.js-Worker und die WebView-Polyfills (`@oleafly/preview/polyfills`, in `main.tsx` zuerst importiert) | `onOpenLink`-Prop (Links an den Systembrowser), `setPdfLogger` | `pdfjs-dist` |
| `@oleafly/diagram` | Der visuelle Diagramm-Composer für TikZ, Typst (fletcher) und Mermaid: React-Flow-Canvas, Form-Inspektor, Tabs „Zeichnen“/„Code“, Ablauf Kompilieren-Vorschau-Einfügen. Jede Sprache in `src/languages/` hat einen Writer, einen Reader sowie ihr Einfüge- und ihr Dateiformular, und Canvas und Code bleiben in beide Richtungen im Takt | `DiagramHost` (LaTeX-Kompilierung, Typst- und Mermaid-Rendering, Datei-IO, Editor-Einfügen, KI-Fix), `DiagramKit`-Kontext (Button/Tooltip/Select/toast/theme) | `@xyflow/react`, `@codemirror/*`, `@oleafly/latex`, `@oleafly/editor` |
| `@oleafly/ai-tools` | Werkzeugdefinitionen für Projekt, Recherche, Gedächtnis und Abbildungen mit genehmigungsbewussten Mutationen | `AiToolsHost` (Dateien, Kompilieren, Symbolindex, Abbildungs-Pipeline, Editor) | `@oleafly/latex` |
| `@oleafly/templates` | Die Vorlagengalerie für neue Projekte (zweistufiger Assistent, Kategorien, Vorschauen, einmalige Asset-Downloads mit Fortschritt) | `TemplatesHost` (Vorschauen, Asset-Downloads, Logging), `TemplatesKit` (Button/Tooltip), Farboptionen als Props | keine außer UI-Utilities |

Abhängigkeitsrichtung, strikt erzwungen:

Ein Paket darf niemals aus `src/` (`@/…`) importieren, weder aus einem Zustand-Store, aus `@tauri-apps/*` noch aus den UI-Komponenten der App. Der Reinheitscheck ist Teil der Extraktionshygiene:

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

## Das Port-Muster

Pakete deklarieren Schnittstellen. Die App implementiert sie einmal, üblicherweise als Adapter-Objekt auf Modulebene, das die Stores per Closure einschließt. Vier wiederkehrende Formen:

**Host-Ports**: ein `interface XHost` pro Paket, das die benötigten Dienste abdeckt. Die App baut den Adapter mit imperativen Store-Lesezugriffen:

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

App-Anliegen im Adapter zu halten hält auch Abhängigkeiten aus den Paketen heraus. Provider-Zugangsdaten und -Anfragen bleiben in Rust, sodass Frontend-Pakete nur den Zustand und die Ergebnisse erhalten, die ihre Schnittstellen verlangen.

**UI-Kits**: ein React-Kontext (oder eine Prop) mit strukturell typisierten Komponenten: `Button: ComponentType<{ variant?: …; onClick?: () => void }>`. Die App reicht ihre shadcn-Komponenten unverändert durch, weil der kleinere strukturelle Typ kompatibel ist. Kits tragen außerdem `toast` und einen `useThemeMode()`-Hook.

**Hook-förmige Ports**: für Pakete, die App-Zustand *abonnieren* müssen. Der `EditorHost` des Editors hat Hook-Mitglieder (`useActivePath()`, `useSettings()`) plus imperative Zwillinge (`getActivePath()`) für Event-Listener. Regeln: Das Host-Objekt muss auf Modulebene liegen (stabile Identität), und seine `use*`-Mitglieder müssen Hooks bedingungslos aufrufen.

**Modul-Singletons**: für übergreifenden Klebstoff; `setPdfLogger(fn)`, `setSpellHost(host)` und `setBibKeysProvider(fn)` haben No-op-Defaults und werden vom App-Shim als Import-Nebenwirkung installiert.

### Shims

Jede Extraktion hat am alten `@/…`-Pfad eine dünne Datei hinterlassen, die das Paket re-exportiert (oder wrappt), damit Konsumenten und bestehende `vi.mock(...)`-Pfade nie umgebaut werden mussten. Beispiele: `src/components/editor/cm/controller.ts` (Re-Export), `src/components/pdf/PdfViewer.tsx` (Wrapper, der den Link-Öffner des Shell-Plugins injiziert), `src/lib/ai-tools.ts` (Adapter + ursprüngliche Factory-Signaturen). Neuer Code darf `@oleafly/*` direkt importieren. Shims existieren der Kompatibilität wegen, nicht als bevorzugter Pfad.

## Die Contribution-Registry

`@oleafly/registry` ist der Weg, auf dem sich Features in die App-Shell einklinken, ohne dass die Shell sie kennen muss. Drei Arten von Contributions:

- **Rail-Tabs** (`registerRailTab`): Icon, Section (`explore` / `review` / `assist`, Trennlinien werden zwischen nicht-leeren Sections gezogen), optionale Sichtbarkeit über `when(ctx)`, optionaler `useBadge()`-Hook (z. B. die Anzahl der Git-Änderungen) und die `panel`-Komponente der Sidebar.
- **Befehle** (`registerCommand`): werden über `surfaces` der Befehlspalette (Cmd+K, nach `group` gruppiert) und/oder der Omnibar (Cmd+P) angeboten. Labels und Icons können Funktionen des Kontexts sein (z. B. „Zum hellen Design wechseln“).
- **KI-Toolsets** (`registerAiToolset`): ein Chatmodus (`"chat"`, `"figure"`), der auf eine Toolset-Factory abgebildet wird. Das Chat-Panel schlägt das Toolset des aktiven Modus nach, statt eines festzuverdrahten.

`AppContext` (`{ projectId, projectKind, theme }`) wird von jeder rendernden Oberfläche aus ihren eigenen Abonnements gebaut, sodass `when`/`label`-Bedingungen reaktiv bleiben.

Die Registrierung ist **statisch**: `src/contributions/` registriert alles Eingebaute (Tabs, Befehle, Toolsets), und `main.tsx` ruft `registerContributions()` einmal auf, bevor die Shell gemountet wird. `Rail`, `Sidebar`, `CommandPalette`, `SearchOmnibar` und `ChatPanel` rendern nur das Registrierte.

Um das Tab oder den Befehl eines Features hinzuzufügen, registrieren Sie es. Keine Bearbeitung der 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"),
});
```

Das ist eine interne API, kein Plugin-SDK: Formen dürfen sich frei ändern, und nichts wird dynamisch geladen. Zurückgestellt, bis es einen echten Konsumenten gibt: Exportmenü-Formate, Einstellungs-Schemas und Projektart-Verhalten als Contributions.

## Modulauflösung

Weil Pakete als Quelltext konsumiert werden, wird derselbe Alias an drei Stellen deklariert, und alle drei müssen synchron bleiben, wenn ein Paket hinzukommt:

1. `tsconfig.json` → `compilerOptions.paths`. Subpaths wie `@oleafly/preflight/pdf-extract` brauchen einen eigenen expliziten Eintrag.
2. `vite.config.ts` → `resolve.alias`. String-Aliase matchen nach Präfix, sodass Subpaths ohne weiteren Eintrag auflösen.
3. `vitest.config.ts` → derselbe `resolve.alias`, **plus** `test.include` muss `packages/**/*.test.ts` abdecken. Andernfalls fallen verschobene Tests still aus dem Lauf heraus.

Außerdem: Das Wurzel-`package.json` hängt per `"workspace:*"` von jedem Paket ab, und `src/styles/globals.css` enthält `@source "../../packages"`, damit Tailwind v4 die Paketquellen nach Klassennamen scannt.

Nur in Vite existierende Importe (`?worker&url`-Worker, CSS-Importe) funktionieren aus Paketquellen unverändert, weil die Aliase auf den Quelltext zeigen. Halten Sie alles, was pdf.js mit sich zieht, aus Barrels heraus, die Node-Umgebungstests importieren: Deshalb sind `pdf-extract` und der Viewer des Preview-Pakets nur dort erreichbar, wo das DOM existiert.

## Arbeiten an Paketen

- **Tests** liegen neben dem Quelltext (`packages/*/src/*.test.ts`) und laufen im selben `pnpm test` wie die App-Tests. Achten Sie beim Verschieben von Dateien auf die *Anzahl* der Tests.
- **Abhängigkeiten** werden locker deklariert (`"react": "*"`). pnpm dedupliziert gegen die Wurzel, wo die echten Versionsuntergrenzen liegen. Deklarieren Sie keine Abhängigkeiten, die das Paket nicht importiert.
- **Gates für jede Paketänderung**: `pnpm exec tsc --noEmit`, `pnpm build`, `pnpm test` (unveränderte Anzahl) und der Reinheits-grep von oben.
- **Etwas Neues extrahieren**: Verschieben Sie Dateien mit `git mv` (die Historie folgt), definieren Sie die Ports, lassen Sie Shims an den alten Pfaden zurück, verdrahten Sie die drei Aliase und führen Sie dann die Gates aus. Extrahieren Sie kein Feature, an dem Sie gerade aktiv iterieren.
- **Lizenzierung**: Jedes Paket steht unter `AGPL-3.0-or-later`, wie die App.

## Was in der App bleibt

`src/` behält alles, was wirklich die Frontend-App ausmacht: die Zustand-Stores, den Tauri-IPC-Client (`src/lib/tauri.ts`), das shadcn-UI-Kit, Layout-/Shell-Komponenten, die Port-Adapter und Shims, `src/contributions/` und Feature-Klebstoff, der naturgemäß an Stores koppelt (das Preflight-Panel-UI, das Inline-KI-Bearbeitungs-Plugin sowie Code- oder Hover-Intelligenz, die als `extraExtensions` in den Editor injiziert wird). Tauri-Befehle und die Integration in das Betriebssystem bleiben in `src-tauri/`. Provider-Clients, Streaming und Agent-Orchestrierung gehören in `crates/oleafly-agent/`, damit Secrets und Provider-Anfragen außerhalb der Webview bleiben.
