A2App is a system for building and running agentic applications. It composes an inference endpoint, a shared workspace, an app runtime, and a coordination harness into a coherent whole.
Five capabilities define the system, independent of any particular implementation:
One concrete way to implement A2App maps the four features onto three components:
A2App is built on pi, a coding agent harness that runs LLM inference and exposes agent tool-use capabilities. Install it via npm:
npm install -g @earendil-works/pi-coding-agentVerify the installation:
pi --versionThis repository includes a pi extension at .pi/extensions/makepad/ that teaches pi about the a2app_harness tools — launching Makepad Splash mini-apps, inspecting their widget trees, clicking buttons, sending prompts to sub-agents, and receiving streaming responses. It is loaded automatically when pi runs inside this project directory.
The extension registers custom tools (launch_makepad_app, check_debug_app, launch_app_with_agent, etc.) and an auto-handler for splash-to-agent communication. The workflow goes like this:
- You describe an app in the pi chat — e.g. "build a counter app with +/- buttons and send the count back to me when I click Send".
- Pi generates Splash DSL — a simple layout language (View, Label, Button, TextInput) and calls
launch_makepad_appto render it inside the Makepad host window. - Pi inspects the UI — it takes a
widget_snapshotto discover widget positions (orphaned widgets have window-relative coordinates), then simulates clicks and text input to verify the app works. - Interactive apps communicate back — splash buttons call
ui.__pi_response.set_text(...)to send data to pi. Pi reads responses viainspect_makepad_docor the asyncwait_for_responsetool. - AI-powered apps — using
launch_app_with_agent, pi creates a blank-slate sub-agent session. The splash app sends prompts viaai:ask:messages, and the sub-agent's response streams token-by-token into an auto-displayed widget.
cargo build -p harness -p makepad-hostStart pi in this project directory:
piThen say something like:
Launch an AI chat app (using a sub agent).
Pi will generate the Splash DSL, launch it inside the Makepad host window with a blank-slate sub-agent session, and the response will stream token-by-token into the app.
A — Coding Agent handles inference (feature 1), workspace access (feature 2), and the main agent chat UI (feature 4). It runs the LLM loop, provides the user-facing chat interface, reads and writes the filesystem, and communicates outward via a WebSocket connection to the Rust process.
B — Rust Process is the harness (feature 5). It runs a WebSocket server that the coding agent connects to, and maintains a CRDT for state synchronisation with the app runtime. The CRDT could in principle live in the coding agent, but cross-language friction made a WebSocket bridge the pragmatic call.
C — App Runtime covers the runtime (feature 3). It hosts a Makepad application shell capable of launching Splash mini-apps, and consumes state from the Rust process via the CRDT.
The system has three components: a coding agent (pi), a harness (Rust bridge process), and a host (Makepad app runtime). Communication flows: pi ↔ harness (JSON WebSocket, port 2341) and harness ↔ host (samod CRDT sync, port 2342). The CRDT is an implementation detail used to keep the host's UI state in sync with the harness — all messages between pi and the host pass through the CRDT document (AgentDoc), but pi never interacts with the CRDT directly.
Sends a {"type": "launch"} message over JSON WS to the harness, which writes the splash body into the CRDT pending_app field. The host receives the CRDT update via samod sync, evaluates the Splash DSL, and renders it in an AgentSplash widget. The CRDT here acts as a write-once command queue: the harness writes, the host reads and processes, then clears the field.
Like the above but also creates a sub-agent session in the pi extension. The splash body calls ui.__pi_response.set_text("ai:ask:" + message) which writes to the CRDT user_response field via the host. The harness detects the version increment and forwards the response to pi over JSON WS. The pi extension's auto-handler routes it to the sub-agent. When the sub-agent responds, streaming deltas are sent back through the harness (as send_streaming_delta → CRDT streaming_text) and the host's sync_streaming_text() method updates the UI on Event::Signal.
Sends a {"type": "debug"} message over JSON WS. The harness writes debug_command to the CRDT doc. The host's process_debug_commands() executes the command and writes the result back to debug_response, which the harness forwards to pi. For click/type_text interactions, the harness also sets a pending_interaction flag so the bridge loop waits for the host to process before reading stale user_response values.
| Command | Executed By | Returns |
|---|---|---|
widget_snapshot |
Host reads cx.widget_tree() |
JSON array of widgets (id, type, position, size, text, value) |
click |
Host dispatches synthetic MouseDown/MouseUp to splash.handle_event() |
Debug response after execution |
type_text |
Host walks splash children depth-first, fills first TextInput | Debug response after execution |
Sends a {"type": "get_doc"} message over JSON WS. The harness reads the current CRDT AgentDoc state directly and returns app_id, user_response, error_message, and status. This is synchronous — no CRDT sync needed because the harness owns the doc.
The host's AgentSplash injects these widgets into every splash body via SPLASH_PREFIX / SPLASH_SUFFIX:
-
__pi_response := Label{text:""}— the splash app callsset_text()on this Label to send data back to pi. The host detects the text change inhandle_event()(on every event type), writes it to the CRDTuser_responsefield, and incrementsuser_response_version(to detect same-value re-sends like toggles). The harness bridge loop compares version numbers and forwards the response to pi over JSON WS. -
__pi_data := Label{text:" "}— the splash app readstext()from this Label, which gets its value from the CRDTpi_responsefield. Written by the harness when pi sends{"type": "send_pi_response"}, synced to the host on the nextEvent::Signal. -
__ai_text := Label{text:" "}— auto-displays sub-agent responses. Updated bysync_streaming_text()from the CRDTstreaming_textfield (live streaming deltas) andsync_pi_data_to_splash()frompi_response(final response). -
__run_splash := AgentSplash{is_root:false}— a nested AgentSplash that evaluates and renders\``runsplashcode blocks inline. Called bysync_streaming_text()during streaming andsync_pi_data_to_splash()on completion. Has built-in error recovery: ifeval_bodyfails,set_text()` restores the previous valid body.
When a sub-agent responds to an ai:ask: prompt:
- The pi extension's per-prompt subscription captures each
text_deltaevent - Each delta (raw new characters) is sent to the harness as
{"type": "send_streaming_delta"} - The harness appends the delta to the CRDT
streaming_textfield - The CRDT syncs to the host via samod WebSocket
- On receiving
Event::Signal, the host callssync_streaming_text()which:- Compares
self.last_streaming_textwith the current CRDT value - If changed: updates
__ai_textLabel and extracts\``runsplash` code blocks - Passes extracted code to
__run_splash.set_text()for inline rendering (error recovery on partial code)
- Compares
- On completion, the extension sends
{"type": "send_streaming_end"}; the harness setspi_responseto the final text and clearsstreaming_text
Key design: CRDT reads only happen on Event::Signal (not on 60fps Draw/Mouse events) to avoid CPU jank.
Five splash apps launched and tested, from simple to meta. Full session log at app_gen.jsonl.
1. Counter — counter-simple
Launched with launch_makepad_app. Interactive +/- buttons using let count = 0 variable persistence. Clicked with check_debug_app(debug_command="click") using window-relative orphan coordinates from widget_snapshot. "Send to Pi" button demonstrated splash→pi communication via __pi_response.set_text(), verified with inspect_makepad_doc.
2. Todo List — todo-1
Launched with launch_makepad_app. Items added by typing into a TextInput via check_debug_app(debug_command="type_text") then clicking "Add". Uses while loop over a struct array with items.push()/items.remove() for list management. "Remove Last" removes items, "Send to Pi" returns the full list via __pi_response.
3. AI Chat — chat-ai-1
Launched with launch_app_with_agent (creates a blank-slate DeepSeek V4 Flash sub-agent). Splash body sends __pi_response.set_text("ai:ask:" + msg) which the extension auto-handler routes to the sub-agent. Response streams token-by-token into the injected __ai_text widget via the streaming delta system. Asked "What is a CRDT?" — got a full streaming response with bullet points.
4. 🌟 Splash Generator — splash-gen-1
Launched with launch_app_with_agent using a system prompt teaching correct Splash DSL syntax (:= naming, on_click:||{}, width:Fill, no commas). Asked for "a simple counter with + and - buttons". The AI generated valid \`\`\`runsplash code which was automatically picked up by sync_streaming_text() and rendered inline via the nested __run_splash AgentSplash — height grew from 0 to 286px.



