Appearance
Architecture
Pomelo is one Rust program. The app and the pom CLI are built from the same crates, so there is no daemon, no background server and no localhost port between the UI and the engine. The core turns your pom.yml into running, isolated per-branch environments and drives the tools already on your machine.
High level
The pieces
- The app (
crates/pomelo) is a thin composition root: it opens the windows and wires the feature crates together. It holds no feature logic. - UI toolkit -
uidraws everything on the GPU (wgpu + winit) from an element tree, with a bundled UI font so text renders the same on every Mac.workspaceis the window layout: the WORKSPACES sidebar, docks, pane groups with tabs and splits, the status bar and the keymap.editoris the code editor core (rope buffer, tree-sitter highlighting, multi-cursor, undo). - Feature views - one crate per screen:
files_ui(the center editor, file finder, project search),git_ui,pull_request_ui,services_ui,database_ui,terminal_ui,settings_ui,jira_ui,markdownand more. - Core crates hold the logic, with no rendering:
pom_config- readspom.yml, resolves templates, validates, and makes checked edits (normalize, rename alias, remove repo).pom_core- projects, new-project scaffolding, adding and removing repos.pom_services- the service runner, env files, ports and the shared services: Docker containers, and commands run once for every workspace.pom_workspace- the staged workspace create and delete pipelines.pom_ptyhost- Pomelo's own PTY holders.pom_proxy- the dev-proxy and webhook relay.pom_mcp- the MCP server agents use.pom_agent- launching agents, their hooks and state.pom_detect- stack detection that drafts a new project'spom.yml.pom_doctor- the config doctor.auto_update- verified self-updates.
- The
pomCLI (crates/pom_cli) drives the same crates from a terminal. A service started withpom startshows up in the app, and the other way round. It ships inside the app bundle atPomelo.app/Contents/MacOS/pom.
Processes
The app re-runs its own binary for the helpers it needs, so nothing else has to be installed:
pty- every service, terminal and agent runs in a PTY holder: a detached process behind a Unix socket. Holders outlive the app, so services keep running and terminals reattach after a restart.mcp- the stdio MCP server a coding agent talks to. On launch the app registers it in~/.claude.json.claude-hook- Claude Code's hooks call it on each event to record the agent's state. The app installs the hooks in~/.claude/settings.json.
Only the dev-proxy (127.0.0.1:8767) and the webhook relay (127.0.0.1:8766) listen on a port. Settings > Dev Services moves or turns them off; POM_WEB_PORT overrides both: the relay takes that port + 1 and the proxy + 2.
How ports are handed out
The app, every pom command and every agent's mcp server run as separate processes, so a service's port lives on disk where all of them read it, never only in one process's memory:
ports.d/<port>reserves a number for the whole machine. It is created exclusively, so two services never get the same port.keys.d/<hash>names the one port a service owns and the process running it. It is put in place atomically: when two processes ask for the same service at once, one wins and the other reads its port.
The dev-proxy sends a request to that port when something answers there, otherwise to the port the service's processes really listen on (a server that ignores $PORT, or one on ::1 only), and caches the answer for a few seconds. The app checks every lease of the project every 5 seconds: a port stays while its service's process lives, even through a long rebuild; it goes back once the service has stayed down for 20 seconds, failed to come up within 45, or was never started for 7 days. Leftover duplicates from older versions are merged when the app opens.
Where things live
| Path | What |
|---|---|
~/pom/<name>/ | A project created in the app (pom.yml, workspace--<branch>/ folders). POM_SESSIONS_ROOT moves it. |
~/.local/state/pom/ | Runtime state: ports (ports.d/, keys.d/), the session list, secrets, agent states. XDG_STATE_HOME moves it. |
~/.config/pomelo/settings.json | App settings. |
~/.config/pomelo/keymap.json | Your key bindings. |
~/.config/pomelo/themes/ | Your own themes. |
~/.config/pomelo/running-Pomelo.json | Marks a running app so the next launch can tell a crash at startup; removed on a clean quit. The dev build keeps running-PomeloDev.json. |
~/.config/pomelo/quiet-language-servers.json | Missing language servers you chose Don't show again for. |
~/.config/pomelo/dismissed-grammar-suggestions.json | Language packages you chose Don't show again for. |
~/.config/pomelo/recent-languages.json | Languages you opened lately, so their packages install again after an update. |
~/.config/pomelo/auto-installed-grammars.json | Language packages already installed that way. |
~/Library/Application Support/Pomelo/languages/ | Language servers Pomelo downloaded or built. |
~/Library/Application Support/Pomelo/grammars/ | Installed language packages, one folder per version. |
~/Library/Application Support/Pomelo/update/ | A downloaded app update waiting to install. |
~/Library/Logs/pomelo-panic.log | What the app was doing when it crashed, to attach to a bug report. |
POMELO_CONFIG_DIR moves everything under ~/.config/pomelo/. Every file Pomelo keeps is listed in the files reference.
