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 (öffnet in einem neuen Tab) zeigt das Bild aus Frontend/Rust/Sidecar; unter Entwicklung 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
Abschnitt betitelt „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).
Die Pakete
Abschnitt betitelt „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) | 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:
grep -rn 'from "@/\|@tauri\|zustand' packages/*/src # must return nothingDas Port-Muster
Abschnitt betitelt „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:
// 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.
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
Abschnitt betitelt „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 überwhen(ctx), optionaleruseBadge()-Hook (z. B. die Anzahl der Git-Änderungen) und diepanel-Komponente der Sidebar. - Befehle (
registerCommand): werden übersurfacesder Befehlspalette (Cmd+K, nachgroupgruppiert) 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:
// 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
Abschnitt betitelt „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:
tsconfig.json→compilerOptions.paths. Subpaths wie@oleafly/preflight/pdf-extractbrauchen einen eigenen expliziten Eintrag.vite.config.ts→resolve.alias. String-Aliase matchen nach Präfix, sodass Subpaths ohne weiteren Eintrag auflösen.vitest.config.ts→ derselberesolve.alias, plustest.includemusspackages/**/*.test.tsabdecken. 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
Abschnitt betitelt „Arbeiten an Paketen“- Tests liegen neben dem Quelltext (
packages/*/src/*.test.ts) und laufen im selbenpnpm testwie 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
Abschnitt betitelt „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.