I've spent the past several months building PolyUI, a desktop app for chatting with local LLMs through Ollama (it also works with OpenAI-compatible APIs). This post is a quick tour of how it's put together and why I made the choices I did.
Why another chat UI?
Most local-LLM front ends lean on Docker, a Python environment, or both. That's fine for a lot of developers, but it's a lot of friction to hand to a less technical friend who just wants to try running a model locally. PolyUI is a single installer: no containers, no venvs, just an app.
Stack
PolyUI is a Tauri v2 app: a React/TypeScript frontend bundled with a Rust backend, with SQLite (via tauri-plugin-sql) as the persistence layer.
Frontend structure:
- Zustand stores, one per domain (auth, chat, models, settings, folders, theme). Stores don't import each other directly — cross-store effects go through a coordinator module so the dependency graph stays flat and testable.
- Self-contained feature modules (chat, auth, sidebar, models, ollama, settings, memory, providers, folders, web-search, etc).
- A lib layer with no UI: an EventBus interface with a Tauri implementation, a pure token-accumulator that batches streamed text via requestAnimationFrame, and a repository interface for conversations with both a SQLite and an in-memory implementation.
Backend structure:
- Thin Tauri command adapters, with the real logic living in dedicated modules — a tool-calling loop that streams until completion, a StreamEmitter trait with a real Tauri implementation and a test-spy implementation, and separate modules for auth, memory, providers, and model management.
Data flow
Rust emits typed Tauri events (chat-chunk, chat-thinking, web-search-event). The frontend's event bus subscribes to them, a stream accumulator batches the incoming tokens via requestAnimationFrame instead of re-rendering on every chunk, and React hooks consume the accumulator. Multi-model streams each get their own request_id so several models can stream side-by-side without stepping on each other.
Testing
The trait-based seams (StreamEmitter, the repository interface) mean the streaming and persistence logic can be unit tested without spinning up an actual Tauri window — swap in the test spy or the in-memory repository and assert against pure logic.
What it does today
- Multi-model conversations with real-time, side-by-side streaming
- Full Markdown + LaTeX (KaTeX) rendering
- Installing Ollama models from inside the app
- Guest mode for temporary chats that never touch disk
- A handful of built-in system-prompt personas, or a custom one
- Everything stays on-device; nothing leaves your machine without explicit action
It's MIT licensed. Repo's here: https://github.com/monolabsdev/poly-ui — stars, issues, and PRs are all welcome, and I'd love to hear how the architecture holds up if anyone else is building something similar on Tauri.
Top comments (0)