Arquitetura do frontend
Como o Oleafly divide o aplicativo de desktop entre o workspace de frontend, os serviços em Rust e os sidecars fixados.
O frontend do Oleafly é um workspace pnpm: o shell do aplicativo fica na raiz do
repositório (src/), e o código de frontend reutilizável fica em packages/*,
como doze pacotes focados. O backend Tauri fica em src-tauri/, enquanto o
código reutilizável de provedores de IA e de agentes fica em
crates/oleafly-agent/. Veja o diagrama de arquitetura do
README (abre em uma nova guia)
para o panorama de frontend/Rust/sidecar e
Desenvolvimento para os comandos do dia a dia.
Este documento cobre: o que é cada pacote, as regras de inversão de dependência que os mantêm puros, o registro de contribuições que conecta funcionalidades ao shell, como funciona a resolução de módulos e como adicionar ou extrair um pacote.
Por que um workspace
Seção intitulada “Por que um workspace”Antes, cada funcionalidade acessava diretamente os stores Zustand do aplicativo, o cliente IPC do Tauri e o kit de UI shadcn. Isso tornava as funcionalidades inseparáveis do shell: nada podia ser testado, reutilizado ou analisado isoladamente. O workspace inverte isso. Os pacotes concentram a lógica das funcionalidades e declaram interfaces estreitas, chamadas portas, para tudo de que precisam. O aplicativo implementa essas portas sobre seus stores e o cliente Tauri, e as injeta. O aplicativo depende dos pacotes, nunca o contrário.
Os pacotes são consumidos como código-fonte TypeScript (o main deles
aponta para src/index.ts). Não há etapa de build, nem publicação, nem
divergência de versões: Vite, Vitest e tsc resolvem @oleafly/* diretamente
para as fontes dos pacotes por meio de aliases (veja Resolução de
módulos).
Os pacotes
Seção intitulada “Os pacotes”| Pacote | O que é | Portas injetadas | Dependências notáveis |
|---|---|---|---|
@oleafly/latex | Lógica pura de LaTeX/figuras: o modelo de diagramas, o serializador TikZ (modelToTikz, ida e volta de modelos embutidos), o construtor de documentos standalone (buildStandaloneDoc), auxiliares de nome/bytes de figuras | nenhuma (puro) | nenhuma |
@oleafly/ai-core | Metadados de exibição de provedores e seleção de modelos no frontend, mantidos em paridade com o catálogo Rust | nenhuma (puro) | nenhuma |
@oleafly/latex-intelligence | Tipos validados e acesso a dados para o corpus de LaTeX usado pela inteligência do editor | nenhuma (puro) | nenhuma |
@oleafly/registry | O registro de contribuições: abas da rail, comandos da paleta/omnibar, conjuntos de ferramentas de IA (detalhes abaixo) | nenhuma (as contribuições carregam o próprio comportamento) | react (tipos) |
@oleafly/preflight | O motor de pré-verificação: tipagem de documentos, regras de fonte/PDF/referências, parsing de ATS, preparação de exportação acessível, pontuação. pdf-extract é distribuído como o subpath @oleafly/preflight/pdf-extract para que o pdf.js fique fora dos grafos de teste em node | nenhuma (puro) | pdfjs-dist (somente subpath) |
@oleafly/pdf-to-latex | O pipeline local de reconstrução de PDF para LaTeX: agrupamento de linhas, detecção de títulos e colunas, inferência de estilo, mapeamento de símbolos, extração de figuras, escape de caracteres e montagem de documentos. O adaptador do pdf.js é distribuído como o subpath @oleafly/pdf-to-latex/pdf-adapter pelo mesmo motivo do grafo de teste em node | nenhuma (puro) | pdfjs-dist (somente subpath) |
@oleafly/wysiwyg | O núcleo do editor visual restrito: esquema compartilhado, parse/serialização de LaTeX e Markdown e os nós brutos inline/bloco que mantêm o código não suportado visível em vez de descartá-lo | nenhuma (puro) | nenhuma |
@oleafly/editor | O núcleo CodeMirror para LaTeX, Typst e Markdown: CodeMirrorEditor, o controlador da visualização do editor (insertAtCursor, gotoLine, …), linguagens + completions, tema, folding, cabeçalhos sticky, linters, latex-mask, pré-visualização de matemática (KaTeX para LaTeX, Typst para Typst), auxílios de edição de Typst e conversão de matemática de LaTeX para Typst, o modo Visual in-loco para LaTeX e Typst (@oleafly/editor/visual), painel de busca, linters ortográfico e gramatical | EditorHost (modelo de documento + configurações, em formato de hook), SpellHost (verificador ortográfico/Harper/dicionário), setBibKeysProvider, extraExtensions/extraKeymap | @codemirror/*, @replit/codemirror-vim, @replit/codemirror-emacs, katex |
@oleafly/preview | O visualizador virtualizado em pdf.js (PdfViewer), o controlador de páginas do SyncTeX (gotoRect, pageClickToBp), o worker do pdf.js e os polyfills de WebView (@oleafly/preview/polyfills, importado primeiro em main.tsx) | prop onOpenLink (links no navegador do sistema), setPdfLogger | pdfjs-dist |
@oleafly/diagram | O compositor visual de diagramas para TikZ, Typst (fletcher) e Mermaid: canvas React Flow, inspetor de formas, abas Desenhar/Código, fluxo de compilar-pré-visualizar-inserir. Cada linguagem em src/languages/ tem um writer, um reader e seus formulários de inserção e de arquivo, e o canvas e o código permanecem sincronizados nos dois sentidos | DiagramHost (compilação LaTeX, renderização de Typst e Mermaid, E/S de arquivos, inserção no editor, correção por IA), contexto DiagramKit (Button/Tooltip/Select/toast/theme) | @xyflow/react, @codemirror/*, @oleafly/latex, @oleafly/editor |
@oleafly/ai-tools | Definições de ferramentas de projeto, pesquisa, memória e figuras, com mutações que respeitam aprovação | AiToolsHost (arquivos, compilação, índice de símbolos, pipeline de figuras, editor) | @oleafly/latex |
@oleafly/templates | A galeria de modelos para novos projetos (assistente em duas etapas, categorias, pré-visualizações, downloads únicos de assets com progresso) | TemplatesHost (pré-visualizações, downloads de assets, logging), TemplatesKit (Button/Tooltip), opções de cor como props | nenhuma além de utilitários de UI |
Direção das dependências, imposta de forma estrita:
Um pacote nunca deve importar de src/ (@/…), de um store Zustand, de
@tauri-apps/* ou dos componentes de UI do aplicativo. A verificação de pureza
faz parte da higiene de extração:
grep -rn 'from "@/\|@tauri\|zustand' packages/*/src # must return nothingO padrão de portas
Seção intitulada “O padrão de portas”Os pacotes declaram interfaces. O aplicativo as implementa uma única vez, geralmente como um objeto adaptador em nível de módulo que encapsula seus stores. Quatro formas recorrentes:
Portas de host: um interface XHost por pacote cobrindo os serviços de que
ele precisa. O aplicativo constrói o adaptador com leituras imperativas dos
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);Manter as preocupações exclusivas do aplicativo dentro do adaptador também mantém dependências fora dos pacotes. Credenciais e requisições de provedores ficam no Rust, então os pacotes do frontend recebem apenas o estado e os resultados que suas interfaces exigem.
Kits de UI: um contexto React (ou prop) de componentes com tipagem
estrutural: Button: ComponentType<{ variant?: …; onClick?: () => void }>. O
aplicativo repassa seus componentes shadcn sem alterações, porque o tipo
estrutural menor é compatível. Os kits também carregam toast e um hook
useThemeMode().
Portas em formato de hook: para pacotes que precisam assinar o estado do
aplicativo. O EditorHost do editor tem membros em formato de hook
(useActivePath(), useSettings()) mais gêmeos imperativos (getActivePath())
para ouvintes de eventos. Regras: o objeto host deve ficar em nível de módulo
(identidade estável) e seus membros use* devem chamar hooks incondicionalmente.
Singletons de módulo: para a cola transversal, setPdfLogger(fn),
setSpellHost(host) e setBibKeysProvider(fn) têm padrões vazios (no-op) e são
instalados pelo shim do aplicativo como efeito colateral de importação.
Cada extração deixou um arquivo fino no antigo caminho @/…, que reexporta (ou
encapsula) o pacote, de modo que consumidores e caminhos existentes de
vi.mock(...) nunca precisaram mudar. Exemplos:
src/components/editor/cm/controller.ts (reexportação),
src/components/pdf/PdfViewer.tsx (wrapper que injeta o abridor de links do
plugin do shell), src/lib/ai-tools.ts (adaptador + assinaturas originais da
fábrica). Código novo pode importar @oleafly/* diretamente. Os shims existem
por compatibilidade, não como o caminho preferido.
O registro de contribuições
Seção intitulada “O registro de contribuições”@oleafly/registry é a forma como as funcionalidades se conectam ao shell do
aplicativo sem que o shell as conheça. Três tipos de contribuição:
- Abas da rail (
registerRailTab): ícone, seção (explore/review/assist, divisores desenhados entre seções não vazias), visibilidade opcional comwhen(ctx), hook opcionaluseBadge()(por exemplo, a contagem de alterações do Git) e o componentepanelda barra lateral. - Comandos (
registerCommand): servidos à paleta de comandos (Cmd+K, agrupados porgroup) e/ou ao omnibar (Cmd+P) viasurfaces. Rótulos e ícones podem ser funções do contexto (por exemplo, “Mudar para o tema claro”). - Conjuntos de ferramentas de IA (
registerAiToolset): um modo de chat ("chat","figure") mapeado para uma fábrica de conjunto de ferramentas. O painel de chat procura o conjunto de ferramentas do modo ativo em vez de deixar um fixado no código.
O AppContext ({ projectId, projectKind, theme }) é construído por cada
superfície de renderização a partir de suas próprias assinaturas, então as
condições de when/label permanecem reativas.
O registro é estático: src/contributions/ registra cada item embutido
(abas, comandos, conjuntos de ferramentas) e main.tsx chama
registerContributions() uma vez antes de o shell ser montado. Rail,
Sidebar, CommandPalette, SearchOmnibar e ChatPanel renderizam apenas o
que está registrado.
Para adicionar a aba ou o comando de uma funcionalidade, registre-o. Sem edições no 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"),});Esta é uma API interna, não um SDK de plugins: as formas podem mudar livremente, e nada é carregado dinamicamente. Adiado até existir um consumidor real: formatos do menu de exportação, esquemas de configurações e comportamentos por tipo de projeto como contribuições.
Resolução de módulos
Seção intitulada “Resolução de módulos”Como os pacotes são consumidos como código-fonte, o mesmo alias é declarado em três lugares, e os três devem permanecer em sincronia quando um pacote é adicionado:
tsconfig.json→compilerOptions.paths. Subcaminhos como@oleafly/preflight/pdf-extractprecisam de uma entrada explícita própria.vite.config.ts→resolve.alias. Aliases de string casam por prefixo, então os subcaminhos resolvem sem outra entrada.vitest.config.ts→ o mesmoresolve.aliase, além disso,test.includedeve cobrirpackages/**/*.test.ts. Caso contrário, testes movidos saem silenciosamente da execução.
Além disso: o package.json da raiz depende de cada pacote via
"workspace:*", e src/styles/globals.css tem @source "../../packages" para
que o Tailwind v4 examine as fontes dos pacotes em busca de nomes de classes.
Imports exclusivos do Vite (workers ?worker&url, imports de CSS) funcionam
sem alterações a partir das fontes dos pacotes, porque os aliases apontam para o
código-fonte. Mantenha fora de barrels qualquer coisa que arraste o pdf.js para
testes que rodam em ambiente node: é por isso que pdf-extract e o visualizador
do pacote preview só são alcançáveis onde o DOM existe.
Trabalhando nos pacotes
Seção intitulada “Trabalhando nos pacotes”- Testes ficam ao lado do código-fonte (
packages/*/src/*.test.ts) e rodam no mesmopnpm testdos testes do aplicativo. Observe a contagem de testes ao mover arquivos. - Dependências são declaradas de forma flexível (
"react": "*"). O pnpm faz a deduplicação contra a raiz, onde ficam os pisos reais de versão. Não declare dependências que o pacote não importa. - Gates para qualquer alteração de pacote:
pnpm exec tsc --noEmit,pnpm build,pnpm test(contagem inalterada) e o grep de pureza acima. - Extrair algo novo: mova os arquivos com
git mv(o histórico acompanha), defina as portas, deixe shims nos caminhos antigos, conecte os três aliases e rode os gates. Não extraia uma funcionalidade enquanto itera ativamente sobre ela. - Licenciamento: cada pacote é
AGPL-3.0-or-later, igual ao aplicativo.
O que fica no aplicativo
Seção intitulada “O que fica no aplicativo”O src/ mantém tudo o que é genuinamente o aplicativo de frontend: os stores
Zustand, o cliente IPC do Tauri (src/lib/tauri.ts), o kit de UI shadcn,
componentes de layout/shell, os adaptadores de portas e os shims,
src/contributions/ e a cola de funcionalidades que por natureza é acoplada a
stores (a UI do painel de pré-verificação, o plugin de edição com IA embutida e
a inteligência de código ou de hover injetada no editor como extraExtensions).
Comandos do Tauri e a integração com o sistema operacional ficam em src-tauri/.
Clientes de provedores, streaming e orquestração de agentes pertencem a
crates/oleafly-agent/, para que segredos e requisições a provedores permaneçam
fora da webview.