EmpryoEmpryo.beta
1.0k
install
← Blog
engineeringSeptember 3, 2026 · 8 min read

I stopped building panels and let you morph the app

Cells & Morphs are one JSON file that changes the Empryo desktop app. A notebook, your tracker as a board you drag, a pomodoro that chimes. The app validates the file and paints it with its own components. You work the panel with your hands. The agent builds it, then gets out of the way.

PProxySoul

Type "a notebook" into a fresh tab. Watch a notebook show up in the app before you finish reading this sentence. Type in it. Your note stays. Nobody sent a prompt.

Now type "my tracker as a board I can drag". A board appears with your tickets in columns. Drag one to Done. The tracker on the other end of the API moves the ticket too, and the board repaints from the tracker's reply.

Type "a pomodoro that chimes". It counts down, hits zero, chimes, and pings you from the dock.

None of my code knows what a notebook, a tracker or a pomodoro is. That is the whole trick, and this post is how it works.

Everything below is captured from the packaged app driving real files. The captures live in the repo's probe suite and run before every release. The feature ships with an experimental label on it, and the last section says where it still bites.

A notebook cell in its own pane, and nothing else open. A field that stores what you type, a list you edit in place, a focus timer, a toggle, a slider. Zero prompts sent.
A notebook cell in its own pane, and nothing else open. A field that stores what you type, a list you edit in place, a focus timer, a toggle, a slider. Zero prompts sent.

The week I got the same request four times

A Jira board in the sidebar. Notes that stay inside the app. A timer, because you already run one in another window and the alt-tab is where you lose the thread. A release countdown.

Each request made sense. Each one was also a panel I would build for one person and maintain for everyone, forever. After the fourth I stopped answering and started on the thing that builds panels.

The first version was a row of buttons. Delete.

My first cut put buttons on a panel, and each button wrote a prompt. It demoed great. It was useless. A button that says "Show my tickets" and types *show my tickets* into the composer is a slower way to type, and it wakes the expensive model to do the job of one HTTP fetch.

The version that survived has one rule. Every control acts on your click. The author of the panel picks, per control, what the click does:

  • Do it here. A field stores what you type. A list renames, deletes, reorders and ticks itself. A toggle, a slider or a checkbox writes straight into the cell's state. A timer counts down. A search box stores q, the feed's URL reads {{state.q}}, and search becomes results with no model in the loop. A card dropped into a column posts the transition to your tracker's API.
  • Ask the agent. A control can stage a prompt in the composer for you to send, or send it at once. Use this where you need a brain, not a fetch.
  • Show. A control can bring the cell's own panel forward or open one of its dialogs.

Pictures count too. A feed that answers with image URLs paints them: the app fetches each one through the same gate as the feed and shows it in a gallery, as a thumbnail on a row, or as a whole block. Which means a cell can be about music.

Now Playing. A twenty-line script on this Mac serves what Spotify is playing; the cell reads it every three seconds. The cover fills the panel and fades behind the thread, a strip above the prompt dances while the song plays, and the transport buttons post back to the script.
Now Playing. A twenty-line script on this Mac serves what Spotify is playing; the cell reads it every three seconds. The cover fills the panel and fades behind the thread, a strip above the prompt dances while the song plays, and the transport buttons post back to the script.

That rule is also why this is not "a fancy agents.md". A markdown agent file tells a model how to behave. A cell binds to the running app. It has a place in the window, state that you and the agent both write, feeds the app fetches on a beat, key chords, slash commands, and rules the agent gets only when your turn matches them. Think of it as a tiny application that you declare and the app runs. Nothing in it executes.

Real capture. A card dragged from Doing to Done. The drop posts the transition to the tracker (a stand-in Jira here), the board refetches, the card lands.

Declared, never executed

A cell is one JSON file. The app reads it, checks it against a schema, and paints it with the same button, select, tabs and dialog every built-in panel uses, on the app's own tokens. There is no plugin runtime and no script. A cell cannot crash the window or read your keychain.

The schema is strict and forgiving at the same time. A field it does not know gets dropped. A block that fails validation drops alone and the rest of the panel still paints. A cell written for a newer app shows its fallback text instead of vanishing. Save the file and the change lands inside a second. Delete the file and everything goes back.

About twenty blocks make up the vocabulary. Stats, progress, badges, key/value rows, a board of grouped rows, fields, notes, check/toggle/slider, timer, code, gallery, kanban, select, table, tabs, list, chart, markdown, image, links, countdown, and a sandboxed frame that can host an MCP App view. The blocks are boring on purpose. What you build with them is not.

The tracker cell. Sprint numbers from one feed, tickets from another mapped into four columns you drag between, a search field that drives the feed. One file.
The tracker cell. Sprint numbers from one feed, tickets from another mapped into four columns you drag between, a search field that drives the feed. One file.

The part I had to get right

A panel is the visible half. The other half is what a cell does to the agent, and that is where a JSON file in a repo turns into a security question.

A cell can hand the agent rules. Always-on instructions, or rules that attach only when your turn names matching files, runs in a given mode, sits on a branch, or lives inside a ticket desk. Cursor's globs and Copilot's applyTo landed on the same shape, so I used it. A cell can offer read-only HTTP lookups the agent may call. It can put guards on tools, like *no recursive deletes in this repo* or *ask before touching migrations/*. It can hold state the agent writes through a cells tool.

You approve all of that in the app before any of it reaches a model. The approval is a hash of exactly what you read: the rules, the lookups, the guards, the secret names the cell carries and the hosts it sends them to. Change any of it and the app holds the layer again while the panel keeps working. That covers the git pull that turns a harmless board into something that ships your token somewhere new. Pulses and lookups fetch https only (or loopback), refuse redirects into private addresses, and run only in a repo you have trusted. Row text from an API is data. The app fences it as data, so a ticket title can never become an instruction.

I found one hole in my own first cut while writing this. A cell could name any environment variable in a header and send it to any host. Credentials are now part of the reach you approve. A cell that adds a secret or a new host counts as a different cell until you say yes again.

Search to results with no agent. The field stores q, the feed's URL reads it, the app refetches and the tiles repaint. Click one and the picture lands in the composer as an attachment.
Search to results with no agent. The field stores q, the feed's URL reads it, the app refetches and the tiles repaint. Click one and the picture lands in the composer as an attachment.

Where a cell lives, and whose settings win

Three homes. .empryo/morphs/ is yours in this repo. .cells/ is committed, so everyone who clones the repo gets the cell. ~/.empryo/morphs/ is yours in every repo. The most specific home wins by id.

Shared cells only became usable once I split the file from your settings. The file is the declaration. Your settings are an overlay. Your switch, your values and the live state of a team or global cell live in this project's overlay. They never touch the committed file and never leak into your other repos. A global Jira cell keeps a different project key per repo, sits off in one repo and on in the rest, and its sprint phase moves here without moving anywhere else. VS Code readers will recognise the rule: workspace over user over the file. I only noticed the need during an audit, when the agent's state changes turned up inside committed team files.

Building one

You never write the JSON by hand. Switch a fresh tab to Morph mode and describe the panel. The agent in that mode has one job and a short belt. It reads the vocabulary, looks at your repo when the panel is about your repo, writes the file, and the panel appears beside the conversation while you talk. It shows you the file's reach before you approve anything.

Morph mode. A tab that builds the app instead of the code. The ask was a status chip and a /standup command for the board on the left; the tool calls read as blocks snapping into place while it writes the file.
Morph mode. A tab that builds the app instead of the code. The ask was a status chip and a /standup command for the board on the left; the tool calls read as blocks snapping into place while it writes the file.
Morphs & Cells. Every cell in this workspace with its reach, a live preview, a switch per cell, and the approval for anything that touches the agent.
Morphs & Cells. Every cell in this workspace with its reach, a live preview, a switch per cell, and the approval for anything that touches the agent.

Cells travel as files. Export one, paste it to a teammate, drop it in .cells/, or install one from the public directory with a click. Signed cells carry an Ed25519 signature and the app tells you when one is official.

Cells & Morphs are experimental and growing: the vocabulary gets new blocks every release. Build one and show it off on Discord or GitHub.

Docs: Cells & Morphs · Download

← All posts
share this post