- All modules live under the
A2UI.*namespace - No
Implsuffix — use behaviour modules and concrete implementations directly - Example:
A2UI.Transport(behaviour),A2UI.Transport.Local(implementation) - Demo app isolated under
A2UI.Demo.*
- 100-character line limit
@moduledoc,@doc, and@specrequired on all public functions- Use
@moduledoc falsefor internal modules (never omit the attribute) - Pipe chains: prefer pipes for 2+ transformations
- Use
{:ok, value}/{:error, reason}tuples for all business logic - No
raisefor expected/recoverable errors raiseonly for programmer errors (e.g., invalid arguments that indicate a bug)- Graceful degradation in rendering — use fallback values when bindings fail to resolve
- Elixir tests:
mix test - JS tests:
mix bun test(Bun is installed via thebunHex package — do NOT callbundirectly) - All tests:
mix test.all(runs both Elixir and JS) - CI tests:
mix test.ci(Elixir with--warnings-as-errors+ JS)
- Test file structure mirrors
lib/structure - All test modules use
async: trueunless they need shared state - Shared test builders in
test/support/component_helpers.ex - Phoenix component tests use
rendered_to_string/1+ Floki assertions
- Test files live in
test/js/and use Bun's built-in test runner (bun:test) - DOM mocks in
test/js/support/dom.js— usemockElement,mockEvent,addChild - Hook tests in
test/js/hooks.test.js, validator tests intest/js/validators.test.js - JS source:
priv/static/a2ui-hooks.js— exports via CommonJS for test imports
- Run
mix precommitbefore committing (compile, unlock unused deps, format, test Elixir + JS)
- Conventional Commits format:
feat:,fix:,docs:,chore:,refactor:,test: - Keep commits small and focused
- Lives under
dev/demo/and theA2UI.Demo.*namespace - Not part of the published hex package — dev/test only
- Compiled only in
:devand:testenvironments (seeelixirc_pathsin mix.exs) - Config in
config/config.exsis demo-specific, not library config - Run with
mix a2ui.demo
- Hex package name:
a2ui - Version: 0.2.0
- Source: https://github.com/actioncard/a2ui-elixir
- License: Apache-2.0
- Maintainer: Action Card AB
Agent (GenServer / A2A Remote Agent)
│ {:a2ui_message, msg} / PubSub
▼
Transport Layer (A2UI.Transport behaviour)
│
▼
LiveView Process (use A2UI.Live)
├─ handle_info({:a2ui_message, msg}) → SurfaceManager → assign
├─ render: <.surface /> → Renderer walks adjacency list → function components
├─ handle_event("a2ui_action") → EventHandler → Transport → agent
└─ handle_event("a2ui_input_change") → updates local data model
│
▼ LiveView WebSocket
Browser (native HTML, phx-click / phx-change events)
- Function Components (not LiveComponents) — surface state managed centrally in LiveView assigns via SurfaceManager
- Pure Functional SurfaceManager —
apply_message(surfaces, msg) → surfaces, no GenServer needed - Data Binding Resolved at Render Time — props stay as raw JSON until render, LiveView diff engine detects changes
- Adjacency List Stays Flat — renderer walks
%{id => component}map via ID lookups, no tree reconstruction - CSS Convention —
a2ui-*BEM-style classes, layout via CSS utility classes, weight via--a2ui-weightcustom property - Transport as Behaviour —
A2UI.Transportbehaviour;Local(process messages),SSE(HTTP),A2A(agent-to-agent)
- Use
Application.get_env/3in library code — accept config via function arguments - Add a supervision tree or
mod:toapplication/0 - Create empty placeholder module files — only create modules when implementing them
- Add
@impl trueon callbacks without a corresponding@behaviour - Put library config in
config/— that directory is demo-only