A team-specialized IDE built from scratch by assembling best-in-class components behind a Rust core that runs identically on the host or inside the workspace's container.
- Built-in support for TS / Rust / Go
- Native git-blame-on-hover
- First-class linters/formatters: oxlint, oxfmt, prettier, eslint (+ plugins)
- In-process coding agent ("coder"): Hugging Face Inference Providers via OAuth device-flow sign-in, container-aware tools, sessions backed by an HF private bucket
- Multi-repo workspaces with cross-repo agent queries
- Containerised dev shells as a first-class concept: terminal/LSP/lint/format/build run in a single per-workspace container, only explicitly forwarded ports cross to the host
- Innovative UIs (the web is the reason we picked Tauri)
- Tauri 2 (Rust backend + webview UI)
- Svelte 5 + TypeScript + Vite frontend
- CodeMirror 6 editor
@pierre/trees(vanilla mode) for the file tree
See specs/architecture.md for the high-level design and specs/ for everything else.
.
├── src/ Svelte 5 UI source
├── src-tauri/ Tauri shell (Rust main, capabilities, config)
├── crates/ Modules
├── specs/ Living design docs
├── AGENTS.md Instructions for AI coding agents working in this repo
├── Cargo.toml Cargo workspace root
└── package.json Frontend deps + scripts
Supported hosts: macOS on Apple Silicon and Linux (x86_64 and arm64). Windows isn't supported.
Common to both:
- Rust 1.90+ (
rustup default stable) - Node 20+ (we use 24)
- Bun (preferred) or pnpm
xcode-select --install
brew install rust bun# Linux Mint / Ubuntu 24+
sudo apt install -y libwebkit2gtk-4.1-dev libsoup-3.0-dev libgtk-3-dev \
libayatana-appindicator3-dev librsvg2-dev libssl-dev pkg-configWebKitGTK provides the webview the Tauri app loads at runtime, so this set is required at both build and launch time.
bun install
bun run build:bin
./target/release/moon-desktopPhased delivery rule — each phase ends with a hand-back to a human reviewer. AI agents do not start the next phase on their own. See AGENTS.md.
Full details in specs/lsp.md. The short version:
-
Detection is by file extension, mapped to an LSP language id in
src/lib/editor/lspLanguage.ts. Each language is wired to exactly one server (there is no server registry or configuration):Language Server Install TypeScript / JavaScript ( .ts,.tsx,.js,.jsx, …)tsgobun add -D @typescript/native-previewRust ( .rs)rust-analyzerrustup component add rust-analyzerPython ( .py,.pyi)tyuv add --dev tyGo ( .go)goplsgo install golang.org/x/tools/gopls@latestSvelte ( .svelte)svelteserverbun add -D svelte-language-serverTypeScript projects on
typescript@7+work without@typescript/native-preview: discovery falls back to the project-local nativetsc(the same binary astsgo, renamed upstream), version-gated so typescript@6's JS-onlytscis never spawned. JS/TS files additionally get oxlint (oxlint --lsp) as a linter co-tenant running alongsidetsgo. Other file types (CSS, HTML, JSON, Markdown) have no LSP yet — syntax highlighting only (see specs/roadmap.md). -
Servers spawn lazily, one process per
(workspace, language), on the first open of a matching file. Nothing runs for languages you don't touch. -
Binary discovery is ecosystem-idiomatic first, then
$PATH:node_modules/.binfortsgo/oxlint/svelteserver,.venv/binforty,$CARGO_HOME/binforrust-analyzer,$GOBIN/$GOPATH/binforgopls. A project-pinned copy always beats a global install. If nothing is found, a status-bar pill shows a copy-pasteable install hint. -
Container routing: when the workspace shell container is running, servers spawn inside it via
docker exec(so they see the same filesystem the build sees), with automatic per-language fallback to a host server when the binary isn't available in the container. -
Debugging "why isn't my server up?": the bottom-panel Logs view has a per-server
lsp.<language>source with discovery and routing decisions.
Full details in specs/editorconfig.md and ADR 0013. Formatting runs on every editor save (Ctrl+S) — hardcoded on, no toggle. Coder file edits defer the same pipeline to the end of the agent turn. Two stages:
-
.editorconfignormalization (in-memory, always): line endings, trailing whitespace, final newline. -
Formatter chain (against the on-disk file):
- If the project has a lint-staged config (
.lintstagedrc.jsonorpackage.json#lint-staged) with a rule matching the file, those commands run in order — that's the per-repo source of truth (this repo uses oxfmt, prettier, and rustfmt this way). - Otherwise a language-default fallback fires:
rustfmt --edition <detected>for.rs,ruff formatfor.py/.pyi(preferring the project's.venv/bin/ruff),gofmt -wfor.go. No fallback exists for other extensions — a file with no lint-staged rule and no fallback just gets the editorconfig pass.
A missing formatter binary logs a one-time warning and the save proceeds with the normalized bytes.
- If the project has a lint-staged config (
Like LSP, the formatter chain runs inside the workspace shell container when one is up.
A paired phone can drive coder sessions and review work over the companion PWA. On a shared LAN nothing needs setting up: release builds of the IDE auto-spawn a local moon-bridge, and the command palette's "Companion: Pair a phone…" shows the QR.
When the phone and the IDE host don't share a network, run a standing relay on any always-on box behind a TLS front (design: ADR 0035):
# build the relay binary + the PWA it serves
cargo build --release -p moon-bridge
bun run build:companion
# on the relay box (nginx or similar terminates public TLS and
# proxies WebSocket upgrades to this listener)
moon-bridge serve --bind 127.0.0.1:53180 \
--advertise-url wss://bridge.example.com \
--no-idle-exit --web-root /path/to/companion-distNotes:
- Build the binary on a machine whose glibc is not newer than the relay box's (e.g. inside the
moon-baseworkspace container, Debian 12) — a binary built against a newer glibc refuses to start there. --no-idle-exitkeeps the relay up with zero local workspaces (the local auto-spawned bridge must not set it);--advertise-urlis what pairing QRs point phones at.- An enrollment code prints at startup (120 s, single-use). Enter it in the IDE via command palette → "Companion: Connect to remote bridge…"; the IDE stores a token and reconnects on its own from then on. Each open workspace registers itself, so the phone sees them all.
- The keyring backend needs a Secret Service even headless — run under
dbus-run-sessionwith an unlockedgnome-keyring-daemon(see the ADR). - Phone pairing QRs are minted on demand from any enrolled IDE's Companion panel; no relay restart needed.
Used for workspace containers, if not wanting to run dev processes on host machines.
docker build -t moon-base:dev images/moon-base/
- Publish the
moon-baseDocker image to Docker Hub. The workspace dev image (huggingface/moon-base) must actually exist on Docker Hub so a fresh clone can pull it instead of building locally. See images/moon-base/README.md and ADR 0007. - Improve the default model / provider onboarding. Right now the flow assumes you connect to Hugging Face first, and the default model choice after connecting could be better. Ideally:
- Pick a sensible default model automatically after connecting to HF.
- Let the editor be used without connecting to HF at all, as long as a model provider is supplied another way.
- Rework the flow around "set the LLM provider" — connecting to HF becomes one option that's triggered (e.g. via the cloud icon) only when an HF provider is chosen.