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.
@pierre/trees (vanilla mode) for the file treeSee 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:
rustup default stable)relativeWorktrees repo extension) — Ubuntu 24.04 / Mint 22 ship 2.43, so grab the git-core PPA: sudo add-apt-repository ppa:git-core/ppa && sudo apt install gitxcode-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-config
WebKitGTK 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-desktop
Phased 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, …) | tsgo | bun add -D @typescript/native-preview |
Rust (.rs) | rust-analyzer | rustup component add rust-analyzer |
Python (.py, .pyi) | ty | uv add --dev ty |
Go (.go) | gopls | go install golang.org/x/tools/gopls@latest |
Svelte (.svelte) | svelteserver | bun add -D svelte-language-server |
TypeScript projects on typescript@7+ work without @typescript/native-preview: discovery falls back to the project-local native tsc (the same binary as tsgo, renamed upstream), version-gated so typescript@6's JS-only tsc is never spawned. JS/TS files additionally get oxlint (oxlint --lsp) as a linter co-tenant running alongside tsgo. 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/.bin for tsgo/oxlint/svelteserver, .venv/bin for ty, $CARGO_HOME/bin for rust-analyzer, $GOBIN/$GOPATH/bin for gopls. 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:
.editorconfig normalization (in-memory, always): line endings, trailing whitespace, final newline.
Formatter chain (against the on-disk file):
.lintstagedrc.json or package.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).rustfmt --edition <detected> for .rs, ruff format for .py/.pyi (preferring the project's .venv/bin/ruff), gofmt -w for .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.
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-dist
Notes:
moon-base workspace container, Debian 12) — a binary built against a newer glibc refuses to start there.--no-idle-exit keeps the relay up with zero local workspaces (the local auto-spawned bridge must not set it); --advertise-url is what pairing QRs point phones at.dbus-run-session with an unlocked gnome-keyring-daemon (see the ADR).moon-remote)A remote machine can serve its workspaces to the companion with no desktop session (ADR 0059): moon-remote is an enrolled IDE without a webview — same relay protocol, same keyring, same coder. First-time setup on the box:
cargo build --release -p moon-remote # same glibc caveat as the relay
scp target/release/moon-remote box:~/bin/
# on the box
moon-remote login # HF device flow: prints a URL + code
moon-remote enroll --bridge wss://bridge.example.com --code XXXX-XXXX
moon-remote workspace-add --name myproject --folder ~/code/myproject
moon-remote model --standard moonshotai/Kimi-K3 # optional; empty = default
moon-remote serve --workspace myproject
Notes:
state.json, per-workspace session.json, coder sessions), so a machine can flip between desktop and headless serving of the same workspaces — model edits the same picks as the desktop's picker, and a running serve re-reads them on restart.dbus-run-session + gnome-keyring-daemon recipe from ADR 0035.serve from a systemd --user template unit (with loginctl enable-linger), e.g. moon-remote@<workspace>.service with Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=%t/bus and Restart=always.workspace_launch from the phone spawns a sibling serve process for stopped workspaces; a workspace-add while serving shows up on the phone after the unit restarts (Register refresh is a known gap).Workspace management is CLI-first: workspace-add binds a folder,
workspace-remove-folder unbinds one, mcp --workspace <slug> [--enable playwright] lists / toggles MCP servers, and model
sets the model picks. All of these edit the same per-workspace state
the desktop writes; stop the workspace's serve before mutating and
restart it after.
moon-base docker imageUsed for workspace containers, if not wanting to run dev processes on host machines.
docker build -t moon-base:dev images/moon-base/
call/subscribe/workspaces route, plus a management surface to grant/revoke bindings per phone. One relay then serves many IDEs and many phones, each phone bound to only the IDEs it was invited to.git worktree add --relative-paths (git ≥ 2.48, enforced at creation time — the devcontainer's git usually satisfies it), which writes extensions.relativeWorktrees into the parent repo's config. From that moment an older git — for example the host's /usr/bin/git 2.43 on Ubuntu 24.04 / Mint 22 while the worktree was created by the container's newer git — refuses to open the repo at all: status, branch, blame, commit, even config --get fail. We hit this in the field (host-side SCM silently dead on every repo a coordinator had touched; remote_web_url already sidesteps it by reading .git/config as a file). Before release either: detect an old host git up front and surface an actionable "upgrade git" error on folder bind (not a cryptic per-command failure), or bundle/download a modern static git binary and use it for all host-side git. Erroring early is the cheap first step; shipping our own git removes the failure class entirely.moon-base Docker 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.714 commits
4 commits
Rust
56.6%
TypeScript
22.6%
Svelte
19.5%
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.
@pierre/trees (vanilla mode) for the file treeSee 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:
rustup default stable)relativeWorktrees repo extension) — Ubuntu 24.04 / Mint 22 ship 2.43, so grab the git-core PPA: sudo add-apt-repository ppa:git-core/ppa && sudo apt install gitxcode-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-config
WebKitGTK 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-desktop
Phased 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, …) | tsgo | bun add -D @typescript/native-preview |
Rust (.rs) | rust-analyzer | rustup component add rust-analyzer |
Python (.py, .pyi) | ty | uv add --dev ty |
Go (.go) | gopls | go install golang.org/x/tools/gopls@latest |
Svelte (.svelte) | svelteserver | bun add -D svelte-language-server |
TypeScript projects on typescript@7+ work without @typescript/native-preview: discovery falls back to the project-local native tsc (the same binary as tsgo, renamed upstream), version-gated so typescript@6's JS-only tsc is never spawned. JS/TS files additionally get oxlint (oxlint --lsp) as a linter co-tenant running alongside tsgo. 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/.bin for tsgo/oxlint/svelteserver, .venv/bin for ty, $CARGO_HOME/bin for rust-analyzer, $GOBIN/$GOPATH/bin for gopls. 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:
.editorconfig normalization (in-memory, always): line endings, trailing whitespace, final newline.
Formatter chain (against the on-disk file):
.lintstagedrc.json or package.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).rustfmt --edition <detected> for .rs, ruff format for .py/.pyi (preferring the project's .venv/bin/ruff), gofmt -w for .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.
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-dist
Notes:
moon-base workspace container, Debian 12) — a binary built against a newer glibc refuses to start there.--no-idle-exit keeps the relay up with zero local workspaces (the local auto-spawned bridge must not set it); --advertise-url is what pairing QRs point phones at.dbus-run-session with an unlocked gnome-keyring-daemon (see the ADR).moon-remote)A remote machine can serve its workspaces to the companion with no desktop session (ADR 0059): moon-remote is an enrolled IDE without a webview — same relay protocol, same keyring, same coder. First-time setup on the box:
cargo build --release -p moon-remote # same glibc caveat as the relay
scp target/release/moon-remote box:~/bin/
# on the box
moon-remote login # HF device flow: prints a URL + code
moon-remote enroll --bridge wss://bridge.example.com --code XXXX-XXXX
moon-remote workspace-add --name myproject --folder ~/code/myproject
moon-remote model --standard moonshotai/Kimi-K3 # optional; empty = default
moon-remote serve --workspace myproject
Notes:
state.json, per-workspace session.json, coder sessions), so a machine can flip between desktop and headless serving of the same workspaces — model edits the same picks as the desktop's picker, and a running serve re-reads them on restart.dbus-run-session + gnome-keyring-daemon recipe from ADR 0035.serve from a systemd --user template unit (with loginctl enable-linger), e.g. moon-remote@<workspace>.service with Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=%t/bus and Restart=always.workspace_launch from the phone spawns a sibling serve process for stopped workspaces; a workspace-add while serving shows up on the phone after the unit restarts (Register refresh is a known gap).Workspace management is CLI-first: workspace-add binds a folder,
workspace-remove-folder unbinds one, mcp --workspace <slug> [--enable playwright] lists / toggles MCP servers, and model
sets the model picks. All of these edit the same per-workspace state
the desktop writes; stop the workspace's serve before mutating and
restart it after.
moon-base docker imageUsed for workspace containers, if not wanting to run dev processes on host machines.
docker build -t moon-base:dev images/moon-base/
call/subscribe/workspaces route, plus a management surface to grant/revoke bindings per phone. One relay then serves many IDEs and many phones, each phone bound to only the IDEs it was invited to.git worktree add --relative-paths (git ≥ 2.48, enforced at creation time — the devcontainer's git usually satisfies it), which writes extensions.relativeWorktrees into the parent repo's config. From that moment an older git — for example the host's /usr/bin/git 2.43 on Ubuntu 24.04 / Mint 22 while the worktree was created by the container's newer git — refuses to open the repo at all: status, branch, blame, commit, even config --get fail. We hit this in the field (host-side SCM silently dead on every repo a coordinator had touched; remote_web_url already sidesteps it by reading .git/config as a file). Before release either: detect an old host git up front and surface an actionable "upgrade git" error on folder bind (not a cryptic per-command failure), or bundle/download a modern static git binary and use it for all host-side git. Erroring early is the cheap first step; shipping our own git removes the failure class entirely.moon-base Docker 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.714 commits
4 commits
Rust
56.6%
TypeScript
22.6%
Svelte
19.5%