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.

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.

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.
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 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.

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.


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.
