Install · Quick start · Make it yours · Building blocks · Working with an agent · Mods from git · Apps · Example apps · Library · Commands · License
A factory for native desktop apps you reshape while they run. Each app's whole window is written in Luau: Rust and GPUI draw it, check it, and keep it safe, and every panel, split, toolbar and status bar is a Luau file that reloads as you save, with its state kept. Ask an agent for a change, or make it by hand, and the running app reshapes itself, even from inside the app: its Send feedback button hands what you write to a coding agent, which changes the app in front of you.
Moonframe starts you off with an app of its own, ready to change, and builds
whole new ones beside it: each runs as a profile of its own and ships as a
standalone release. examples/ holds 25 of them, from a mail client to an
infra console, all in one gallery with just demo. A Rust app can also use
Moonframe as a library.
The window that ships. Every part of it is a mod in app/mods/: the explorer, the terminal tabs, the changes and their diffs, and the status bar widgets, whose CPU, memory and git data come from providers. + opens more tabs: terminals, browsers, files; a markdown file opens in an editor with a live preview.
Themes. Pick one of the bundled color schemes from the palette button or View > Themes…; the window, its controls and the terminals all follow it. On Omarchy the desktop's theme is the default, and changes with it. style.luau restyles anything on top.
|
Components. Ready-made pieces mods use, browsed from View > Components… in Developer Mode with a live preview, the code to paste, and Copy for agent.
|
The example apps, in the gallery just demo opens: a click opens one as a window of its own, beside the others. See Example apps.
- A window laid out in Luau. A shell (
app/main.luau) places regions, and mods fill them. Each mod is one file with its own state,init,updateandview, like an Elm program; mods talk with messages. - Live reload. Save a file and the window redraws in place, keeping each mod's state. A mistake never blanks or shifts the window: a bad element shows as a small warning mark, and a broken mod keeps its last good view.
- A stylesheet. Colors, corners, shadows, spacing, fonts and animations
in
app/style.luau, with rules by element type and class, hover states, transitions on panels, and styles a mod brings for itself. - Over 140 components that mods share with
use "name": tabs, tables, charts, a command palette, a markdown editor, dialogs, sheets, toasts, calendars, chat messages and more, each with a live preview in the Components window. - Terminals.
ui.terminalruns your login shell onalacritty_terminalor on Ghostty's libghostty-vt. With herdr-core or tmux installed, terminals keep running after the app quits and come back on the next launch. - Providers for data Luau can't reach, publishing what views read as
app.<name>: sandboxed WebAssembly (WASM) components, plain processes, or Rust built into the app. The built-in ones cover git (status, diffs, commit and push), GitHub and GitLab, the project's files and markdown documents, the system's load, and coding agents. - Coding agents in the app. The
acpprovider runs Claude, Codex, Gemini, OpenCode or Copilot over the Agent Client Protocol for any mod, andagent-runshows what each is doing: its plan, tool calls, permission questions and answer. - State that lasts. Each mod's state is kept in SQLite across launches, and survives a reload.
- An inspector. View > Inspect Element picks anything in the window, like a browser's developer tools, and tells an agent which file and line drew it.
- Your folder over the app.
~/.config/moonframeholds your own mods, providers and stylesheet, laid over the built-in app; updates never touch it, and mods install from git into it. A file there wins over the app's at the same path, so to change a built-in one, copy it there: the window reloads with the copy, and removing it brings the app's back. The installed app itself stays signed: agents are kept out of it and work on such copies.moonframe doctornames what in your folder breaks the app, and--fixmoves it aside. - A skill for agents in
.claude/skills/moonframethat teaches them to build with all of the above. The app offers to copy its skills to~/.config/moonframe/.claude/skills(only if you agree, and again when an update brings newer ones), so an agent working on your own mods knows them too. - A sandbox. Luau runs with no files, network or process access, a memory cap and an instruction budget; anything that reaches the machine goes through a provider or an element the host owns. WASM providers run sandboxed too, so a provider you install from someone else can have no access to your machine at all. See What runs with what access.
On macOS 13 or later, with Homebrew:
brew install moonframehq/tap/moonframeThis installs Moonframe.app in /Applications and the moonframe command.
Linux and Windows builds are on the
releases page. To change
Moonframe itself rather than build on it, start from the source instead.
You need Rust (the version in rust-toolchain.toml is picked up by rustup)
and just. mise
installs the rest from mise.toml.
git clone https://github.com/moonframehq/moonframe my-app
cd my-app
mise install # Rust, and Zig for the Ghostty terminal
just run # opens the window on app/Now edit app/mods/explorer/main.luau or app/style.luau and save: the
window changes as you type. just check loads everything without a window
and points at any problem, and just types type-checks the Luau.
The Ghostty terminal builds libghostty-vt from source with Zig 0.17 (pinned
in mise.toml). To build without it, use
cargo run -p moonframe-app --no-default-features --features moonframe/default.
For a lean app, select only its capabilities; see
feature flags.
Moonframe is developed on macOS. CI builds, lints and tests it on macOS, Linux (x86_64 and ARM64) and Windows (x86_64), and each release ships a macOS disk image, Linux tarballs and Windows zips, with checksums, Sigstore signatures and GitHub attestations; see docs/releasing.md.
Moonframe is meant to be copied and renamed:
- The app. Replace what's in
app/:main.luaulays out the window,mods/fills it,style.luaustyles it. Keep what helps and delete the rest; the explorer, the tabs and the changes are a place to start. - The name. The crate names and
repositoryinCargo.toml, the app idAPP_IDand the user folder's name (Identity::starter) incrates/moonframe-packages/src/lib.rs, the bundle's name inscripts/bundle-app.sh, and the title inapp/main.luau. - The icon.
assets/icon.png, drawn byscripts/draw-icon.swift. - Ship it.
just bundlebuilds a.appwith your app inside, andjust installputs it in/Applications.
A mod is a folder with one file:
-- app/mods/counter/main.luau
return mod {
region = "left",
init = function(previous)
return previous or { count = 0 }
end,
update = function(state, msg)
if msg == "add" then
state.count += 1
end
end,
view = function(state, app)
return ui.panel {
title = "Counter",
ui.text(`Clicked {state.count} times`),
ui.button { "Add", msg = "add", variant = "primary" },
}
end,
}Save it and it appears in the left region. From there:
| Need | Write |
|---|---|
| A look | a rule in style.luau: panel = { radius = 12, shadow = "0 4px 12px #0003" } |
| A reusable piece | components/<name>.luau, used with local stat = use "stat" |
| A shell | ui.terminal { id = "main", cwd = "~/code" } |
| Data from the machine | a provider in providers/<name>/, read as app.<name> |
| A sheet or menu | ui.popover { open = state.open, dismiss = "close", content = sheet, widget } |
| A new kind of element | Rust in crates/, the one change that needs a rebuild |
The skill in .claude/skills/moonframe/SKILL.md is the full reference:
every element, the stylesheet, components, terminals, providers, and
recipes.
app/ the built-in app: shell, mods, components, providers, style
crates/moonframe-core/ reads Luau into checked element trees, styles, components
crates/moonframe/ the library: the window, drawn with GPUI, inspector, menus
crates/moonframe-app/ the `moonframe` binary: the starter, on the library
crates/moonframe-providers/ runs Wasm, process and native providers
crates/moonframe-packages/ installs packages, finds profiles, updates and bundles
crates/moonframe-terminal/ shells on alacritty_terminal and libghostty-vt
crates/moonframe-mux/ keeps terminals in herdr-core or tmux, else a local pty
crates/moonframe-store/ the state kept across launches, in SQLite
crates/moonframe-git/ the native git provider
crates/moonframe-acp/ coding agents over the Agent Client Protocol
crates/moonframe-docs/ reads and saves the project's documents for the editor
examples/ apps built on it, and a package they share
types/moonframe.d.luau types for the Luau API, used by `just types` and editors
wit/provider.wit the interface a Wasm provider implements
The window reloads while an agent edits it, so you can watch it work.
-
An agent in a tab. From any terminal,
moonframe pane new --agent claude "add a clock to the status bar"opens a terminal tab in the running window (opening the app if it isn't), in your folder,
~/.config/moonframe(the profile's with--profile), and starts the agent there with your prompt. You watch it work in the tab while the window changes beside it.--cwd DIRstarts it somewhere else, and without--agentthe tab is a plain shell. The app types the command into your login shell, so the agent runs as you, with your settings. -
Send feedback. The
?in the title bar (or in a corner of an example app) opens a box: write what should change, attach an element picked with Inspect if it helps, and send. A coding agent in the background changes that app's own files with every permission, checks them, then screenshots your window and looks at the result before it answers; the window reloads with the change. Each request becomes a conversation under Feedback & fixes, so when something is not quite right you reply and the agent carries on from there. -
The skill. Claude Code picks up
.claude/skills/moonframe, which tells an agent where a feature belongs, how to build it, and how to check it withjust checkandjust types. -
"Add it here." View > Inspect Element (⌥⌘I), click an element (↑ and ↓ first for the row, list or panel around it), and say "add a list of branches here". The selection, with its file and line, is saved where the agent reads it.
-
Pick a component. In the Components window, Copy for agent copies a request to add that component at your selection.
-
See it before it is made. Ask for ideas ("show me three takes on the status bar") and the
moonframe-mockupsskill draws them live, side by side, in a Mockups tab of your window. Pick copies your choice for the agent; nothing real changes until then.
Mods and providers can live in their own repositories and install into your folder:
moonframe mod new ~/code/my-mod # a git repo with one mod and a README
moonframe mod link ~/code/my-mod # use it in place, editing live
moonframe mod install github.com/owner/repo # or owner/repo, a git URL, a folder; @ref pins it
moonframe mod list | update | remove NAME | enable NAME | disable NAMEPackages ship source only, so everything they run can be read first.
| Code | Runs | Can reach |
|---|---|---|
| Mods (Luau) | in the host's Luau sandbox, with memory and instruction caps | only the elements and providers the host offers |
WASM providers (kind = "wasm") |
on wasmtime, with 64 MiB and a fresh instruction budget per call | only the host calls listed under capabilities in provider.toml (today system, the machine's load) |
| Process and native providers | as your user | anything you can: git, forges, coding agents, your files |
Write a provider as a WASM component if it only shapes data or if it comes from someone else. A bug or bad actor in it can't read your files or reach the network. Keep process and native providers for things the sandbox can't reach, and read them as you would any script.
The sandbox only covers the running component. Building it runs Cargo as your user, including proc macros and dependency build scripts. The host refuses build scripts in the provider or its local crates, and toolchains named by path, and runs Cargo outside its folders, so no Cargo config there applies. It also refuses any package that ships a binary, so what runs is always source you can read. Bundled apps ship their WASM providers prebuilt and build nothing on your machine.
Apps with no WASM providers can turn off the wasm feature, which is on by
default (see docs/library.md).
Build one: Building WebAssembly (WASM) providers walks through a complete Rust component, Luau messages, live reload and packaging, for people and coding agents. Markdown source.
The starter stays as it is; a whole other app installs beside it as a profile, with its own shell, mods, packages and theme, and runs as an app of its own:
moonframe app install github.com/owner/some-app # clones it and what it requires
moonframe --profile some-app # opens it
moonframe app new ~/code/my-app # start one: moonframe.toml, a shell, a mod, agent skills
moonframe app link ~/code/my-app # run it in place, editing live
moonframe app list | update NAME | remove NAMEAn app is a repository laid out like app/, with a moonframe.toml:
[app]
name = "my-app" # the profile it installs as
title = "My App" # its name in the menu bar and bundle
icon = "icon.png" # its icon (default icon.png)
keep = ["terminal"] # built-in mods it shows; it brings the rest
requires = ["owner/repo@v1"] # packages it installs with it, or "../shared"
version = "1.0.0" # its version, for its bundle and updates
updates = "owner/my-app" # the GitHub repository it is released fromWith updates, the app menu gets Check for Updates…, which offers the
repository's newest GitHub release and, on macOS, installs it over the app's
bundle; see docs/updates.md.
The built-in providers, components and styles are there for it to use; the
built-in mods are not, unless it keeps them. Profiles live in
~/.config/moonframe/profiles/, and your own style.luau follows you
into each.
just bundle-app NAME builds a profile's app as a standalone release:
target/bundle/<Title>.app on macOS, or a prefix with a .desktop file
on Linux, with the app's title, id and icon. A bundle starts with
Developer Mode off: no Components…, Inspect Element or Reload. Developer
Mode in the app menu turns them on live, for anyone who wants to change
their app, and the choice is kept; --release and --dev set it for one
run. Outside a release,
Build App Bundle in the app menu builds the window's own app the same
way and shows it in Finder.
An app's main.luau can also add menus to the menu bar, with
shortcuts, and say what its About panel shows (about = { version, description, website, copyright }); the menu bar carries the app's title.
examples/ holds apps built this way, each with its own name, icon, menus,
About panel and Send feedback button. The dashboards and chats run on sample
data or a scripted agent, labelled as such.
just demo opens them all in one gallery (the screenshot at the top): a
picture of each, and a click opens it as a window of its own, or brings it
forward; Open all opens every one to compare them. Their profiles go in
~/.local/share/moonframe/demo, so your own folder stays as it is. Each app
runs from its folder in place, so a change, yours or an agent's from Send
feedback, shows at once.
Stage (stage): the components on a bento board, with sample data, for stills. View hides the captions and changes the backdrop.
More, each a study of a kind of app:
Sessions, Workbench, Stage, Trellis and Lantern require the projects
package beside them (examples/projects). To run one by hand, link it in
place and open its profile:
cargo build --release -p moonframe-app
target/release/moonframe app link examples/ledger --yes
target/release/moonframe --profile ledger &Profiles share the moonframe binary. An app with Rust of its own (or an
existing GPUI app moving over) depends on the moonframe crate instead,
and is a binary of its own:
fn main() -> std::process::ExitCode {
moonframe::Moonframe::new("notes") // ~/.config/notes, its state, its id
.title("Notes")
.app_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/app"))
.native("index", Index::default) // a native provider: `app.index`
.native_async("feed", Feed::default) // one written with `async fn`, on tokio
.view("editor", Editor::new) // a GPUI view: `ui.view { name = "editor" }`
.run() // the window, --check, mod, app, bundle
}Its app/ folder is laid out like this repository's, and stands on
Moonframe's components, providers and stylesheet (compiled into the
library, so a git dependency needs no checkout beside it). moonframe init DIR starts one, or adds the app folder, types and agent skills to an
existing crate; cargo run --release -- bundle builds its bundle.
Providers can be async fn on tokio, and views can await tokio futures:
it uses the app's own runtime (.tokio(handle), or the one
#[tokio::main] runs it in), else makes one.
examples/host is a small one, and
docs/library.md has the rest.
| Command | Does |
|---|---|
just run |
opens the window on app/, with ~/.config/moonframe over it |
just check |
loads everything without a window and reports problems |
just types |
type-checks the Luau in strict mode |
just providers |
builds and lints the Wasm providers |
just bundle |
builds target/bundle/Moonframe.app |
just bundle-app NAME |
builds the profile NAME's app as a standalone release |
just check-examples |
loads and type-checks the apps in examples/ |
just demo |
opens the gallery of every example app, to open and compare them |
just patterns |
opens the independent pattern browser: categories, search, live previews and Luau source |
just patterns-check |
validates the patterns, their types and the generated web/native catalogs |
just install |
builds the app and installs it in /Applications |
just ci |
formatting, lints, tests, types, providers and check |
just audit |
zizmor and actionlint on the workflows, cargo-deny on the dependencies |
just release |
publishes main as the version in Cargo.toml |
just run-host |
opens the example app on the library, examples/host |
moonframe --help lists the app's own options, such as --user DIR,
--no-user, --profile NAME, --check, --release, and --background
(open without taking focus).
To see where time goes, run with RUST_LOG=moonframe::perf=debug: each
message, provider publish and frame is logged with how long it took and
which views it ran (trace adds each view's Luau and tree time). A message
runs only the views of the mods it reached, and a publish only those that
read that provider's data. Work slower than 8 ms is logged at info even
without it, and slower than 16 ms (a whole frame) at warn.
MIT.


























