Skip to content

I stopped building panels and let you morph the app

A Morph is one JSON file that adds a panel to the Empryo desktop app. A notebook, your tracker as a drag-and-drop board, a timer that chimes. The app checks the file and draws it with its own components. You use the panel directly; the agent only builds it.

I stopped building panels and let you morph the app

You want a notepad next to your agent. Or your ticket board. Or a focus timer, because the one you run in another window is where you lose your train of thought every time you alt-tab.

In Empryo's desktop app you can type "a notebook" into a fresh tab and a notebook appears a few seconds later. Type in it and your notes stay. No prompt goes to a model when you do.

Type "my tracker as a board I can drag". A board appears with your tickets in columns. Drag one to Done. Your tracker's API moves the ticket too, and the board redraws from the tracker's reply.

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

None of Empryo's code knows what a notebook, a tracker or a pomodoro is. This post explains how that works. The feature is labelled experimental in the app, and it's still growing.

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 timesLink to this section

A Jira board in the sidebar. Notes that stay inside the app. A timer. A release countdown.

Each request made sense. Each one was also a panel I'd build for one person and maintain for everyone, forever. After the fourth, I stopped building panels and built the thing that builds them.

The first version was a row of buttonsLink to this section

My first attempt put buttons on a panel, and each button wrote a prompt. It demoed well and was useless in practice. A "Show my tickets" button that types *show my tickets* into the chat is just a slower way to type. It also wakes an expensive model to do the job of one HTTP request.

The version that survived follows one rule: every control acts when you click it. Whoever writes the panel decides, per control, what the click does:

  • Act right here. A text field stores what you type. A list lets you rename, delete, reorder and tick items. A toggle, slider or checkbox saves straight into the panel's state. A timer counts down. A search box stores your query, the feed's URL uses it ({{state.q}}), and results appear without any model involved. Dropping a card in a new column sends that change to your tracker's API.
  • Ask the agent. A control can put a prompt in the chat box for you to send, or send it right away. Use this when you need reasoning, not a fetch.
  • Show something. A control can bring the panel forward or open one of its dialogs.

Images work too. If a feed returns image URLs, the app fetches each one under the same rules as the feed and shows it in a gallery, as a thumbnail, or as a full block. So a panel can even 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.

This is also why a Morph isn't just a fancy AGENTS.md. An instructions file tells a model how to behave. A Morph is part of the running app. It has a place in the window, state that both you and the agent can update, feeds the app refreshes on a schedule, keyboard shortcuts, slash commands, and rules the agent receives only when they apply. Think of it as a small app you describe, which Empryo runs for you. No code in it ever 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.

Described, never executedLink to this section

A Morph is one JSON file. The app reads it, checks it against a schema, and draws it with the same buttons, menus, tabs and dialogs as its built-in panels, in the same theme. There's no plugin system and no scripting. A Morph can't crash the window or read your keychain.

The schema is strict but forgiving. Unknown fields are ignored. If one block is invalid, only that block is dropped and the rest of the panel still appears. A file written for a newer version of the app shows its fallback text instead of disappearing. Save the file and the change appears within a second. Delete it and the panel is gone.

You build panels from around twenty block types: stats, progress bars, badges, key/value rows, grouped boards, form fields, notes, checkboxes, toggles, sliders, timers, code, galleries, kanban boards, dropdowns, tables, tabs, lists, charts, Markdown, images, links, countdowns, and a sandboxed frame that can host an MCP App view. The blocks are plain on purpose. What you build with them doesn't have to be.

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: CellsLink to this section

The panel is the half you see. The other half is the Cell: what the file tells the agent. That's where a JSON file in a repo becomes a security question.

A Cell can give the agent rules. Some always apply. Others apply only when your request touches matching files, runs in a certain mode, happens on a certain branch, or comes from a ticket. Cursor and Copilot use a similar file-matching approach, so I followed it. A Cell can also offer read-only HTTP lookups the agent may call, and add guards on tools, such as *no recursive deletes in this repo* or *ask before touching migrations/*. And it can hold state the agent updates through a cells tool.

You approve all of that in the app before any of it reaches a model. The approval is tied to exactly what you reviewed: the rules, the lookups, the guards, the names of any secrets the Cell uses, and the hosts it sends them to. If any of those change, the app holds the Cell back until you approve again. The panel keeps working in the meantime. That protects you from a git pull that turns a harmless board into something that sends your token somewhere new.

A few more limits apply. Feeds and lookups only use https (or your own machine), refuse redirects to private network addresses, and only run in a repo you've marked as trusted. Text that comes back from an API is treated as data. A ticket title can never turn into an instruction for the agent.

While writing this post I found a gap in my own first version. A Cell could put any environment variable in a request header and send it to any host. Secrets are now part of what you approve. A Cell that adds a secret or a new host is treated as a new Cell until you approve it 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 Morphs live, and whose settings winLink to this section

A Morph can live in three places:

  • .empryo/morphs/ is yours, in this repo.
  • .cells/ is committed, so everyone who clones the repo gets it.
  • ~/.empryo/morphs/ is yours, in every repo.

If two share an id, the most specific location wins.

Shared Morphs only became practical once I separated the file from your settings. The file is the definition. Your settings sit on top of it. Your on/off switch, your values and the live state of a shared or global Morph are stored per project. They never change the committed file and never leak into your other repos. A global Jira Morph can use a different project key in each repo, be off in one repo and on in the rest, and track its sprint separately in each. If you use VS Code, the rule will feel familiar: workspace settings override user settings, which override the file. I only noticed the need when the agent's state changes started showing up in committed team files.

Building oneLink to this section

You don't write the JSON by hand. Switch a fresh tab to Morph mode and describe the panel you want. The agent in that mode has one job and a small set of tools. It reads the list of blocks, looks at your repo if the panel is about your repo, writes the file, and the panel appears next to the conversation while you talk. Before you approve anything, it shows you what the Cell can reach.

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.

Open the library with /morphs in the desktop app. In the terminal app, /cells lists the Cells in your project so you can approve, revoke, or switch them on and off.

Morphs travel as files. Export one, send it to a teammate, drop it in .cells/, or install one from the public directory with a click. A Morph can carry an Ed25519 signature, and the app tells you when a signed one is official.

Morphs and Cells are experimental, and new blocks arrive with most releases. Build one and share it on Discord or GitHub.

Docs: Cells & Morphs ยท Download