Dialup.Page behaviour (Dialup v0.2.0)

Copy Markdown View Source

The behaviour and macro for Dialup page modules.

A page module handles a single route. Place it at the appropriate path under your app_dir and it will be routed automatically:

# lib/app/page.ex         → /
# lib/app/blog/[slug]/page.ex → /blog/:slug

Usage

defmodule MyApp.App.Page do
  use Dialup.Page

  def mount(_params, assigns) do
    {:ok, Map.put(assigns, :count, 0)}
  end

  def handle_event("increment", _, assigns) do
    {:update, Map.update!(assigns, :count, &(&1 + 1))}
  end

  def render(assigns) do
    ~H"""
    <h1>Count: {@count}</h1>
    <button ws-event="increment">+1</button>
    """
  end
end

Callbacks

  • mount/2 — called on every page navigation. Receives URL params and the current assigns (which already include session data set by layouts). Return {:ok, new_assigns} or {:redirect, path} / {:redirect, path, assigns} to navigate before render. Defining mount/1 (assigns only) is also accepted as a convenience.
  • render/1 — renders the page HTML using HEEx.
  • handle_event/3 — called when a ws-event, ws-submit, or ws-change fires.
  • handle_info/2 — called for Erlang process messages (e.g. PubSub, timers).
  • page_title/1 — optional; return a string to set <title>. Return nil to use the application default.
  • agent_state/1 — returns the allowlisted state projection visible to an attached agent.
  • agent_message/1 — explains the page concept, operating flow, and safety constraints to an agent that has no source-code context.
  • agent_grant/1 — defines capabilities, projections, expiry, and version requirements for HTTP MCP session tokens. The default grant is read-only ([:read_scene]); override explicitly for mutating or built-in tools such as lock_ui.

Human and agent projections

Use <.dialup_action> instead of a plain event button when the same operation should be available to an attached agent. Use <.dialup_region> for stable domain areas that need a semantic name, structured data, or action relationships.

<.dialup_action> is the single boundary for what an agent can do: raw ws-event, ws-submit, ws-change, and ws-href elements are never auto-exposed as tools. To make a navigation link agent-operable, declare it with <.dialup_action navigate="/path">; the same declaration renders the human link and generates the navigation tool.

Action modes

Each <.dialup_action> uses exactly one mode:

ModeAttributeRouting
commandcommand={{Context, :cmd}}Build Commanded command → Context.dispatch/1 → remount
setset={%{key: value}}Merge rendered map into assigns (inline HEEx only)
navigatenavigate="/path"Navigate to declared path
actionname={:event}Legacy handle_event/3

See guides/agent-native-app-development.md for Commanded integration and availability derivation.

Return values for handle_event/3 and handle_info/2

Return valueEffect
{:noreply, assigns}Update state, no re-render
{:update, assigns}Re-render the full page
{:patch, id, html, assigns}Replace only the element with the given id
{:redirect, path, assigns}Navigate to another page (session is preserved)
{:push_event, name, payload, assigns}Call a JS hook function and re-render

Module attributes

  • @layout false — disable all layout wrapping (useful for login/fullscreen pages).
  • @static true — serve the page without establishing a WebSocket connection.

Colocation CSS

Place a page.css file in the same directory as page.ex. It is automatically scoped to the page at compile time (no build tool required).

Helpers

use Dialup.Page imports overwrite/2, set_default/2, subscribe/2, declare_action/1, declare_region/1, dialup_action/1, and dialup_region/1.

Summary

Functions

Declares an action that is available to both the browser UI and an attached agent.

Declares a semantic region that an attached agent can reference.

Renders an action button and serializes extra component attributes as event parameters.

Wraps content in a semantic region that agents can read through read_scene.

Derives the canonical navigation action name for an app path.

Merges overwrite_map into assigns, replacing any existing keys.

Merges defaults into assigns, keeping existing values for keys that are already set.

Subscribes to a Phoenix.PubSub topic and registers it for automatic unsubscription on page navigation.

Callbacks

agent_grant(assigns)

(optional)
@callback agent_grant(assigns :: map()) :: map() | keyword()

agent_message(assigns)

(optional)
@callback agent_message(assigns :: map()) :: binary() | map()

agent_state(assigns)

(optional)
@callback agent_state(assigns :: map()) :: map()

handle_event(event, value, assigns)

@callback handle_event(event :: binary(), value :: any(), assigns :: map()) ::
  {:noreply, map()}
  | {:update, map()}
  | {:patch, target :: binary(), rendered :: any(), map()}
  | {:redirect, path :: binary(), map()}
  | {:push_event, event_name :: binary(), payload :: map(), map()}

handle_info(msg, assigns)

(optional)
@callback handle_info(msg :: any(), assigns :: map()) ::
  {:noreply, map()}
  | {:update, map()}
  | {:patch, target :: binary(), rendered :: any(), map()}
  | {:redirect, path :: binary(), map()}
  | {:push_event, event_name :: binary(), payload :: map(), map()}

mount(assigns)

(optional)
@callback mount(assigns :: map()) ::
  {:ok, map()}
  | {:redirect, path :: binary()}
  | {:redirect, path :: binary(), map()}

mount(params, assigns)

@callback mount(params :: map(), assigns :: map()) ::
  {:ok, map()}
  | {:redirect, path :: binary()}
  | {:redirect, path :: binary(), map()}

page_title(assigns)

(optional)
@callback page_title(assigns :: map()) :: binary() | nil

render(assigns)

@callback render(assigns :: map()) :: Phoenix.LiveView.Rendered.t()

Functions

declare_action(opts)

(macro)

Declares an action that is available to both the browser UI and an attached agent.

declare_action name: :increment,
               desc: "Increment the counter",
               params: %{amount: {:integer, default: 1}}

declare_region(opts)

(macro)

Declares a semantic region that an attached agent can reference.

dialup_action(assigns)

Renders an action button and serializes extra component attributes as event parameters.

<.dialup_action name={:add_item} available={@status == :draft} sku="SKU-1" qty="1">
  Add item
</.dialup_action>

Pass navigate to render a navigation link instead of an event button. The same declaration becomes a navigation tool for an attached agent, so the agent can move between pages exactly where a human can click:

<.dialup_action navigate="/docs/concepts">Concepts</.dialup_action>

Navigation actions take no parameters; the destination is fixed at the declaration site. When name is omitted it is derived from the path (e.g. /docs/concepts becomes :navigate_docs__concepts).

dialup_region(assigns)

Wraps content in a semantic region that agents can read through read_scene.

overwrite(assigns, overwrite)

Merges overwrite_map into assigns, replacing any existing keys.

assigns |> overwrite(%{user: user, loaded: true})

set_default(assigns, defaults)

Merges defaults into assigns, keeping existing values for keys that are already set.

assigns |> set_default(%{count: 0, page: 1})

subscribe(pubsub, topic)

Subscribes to a Phoenix.PubSub topic and registers it for automatic unsubscription on page navigation.

Call this inside mount/2 to ensure the subscription is cleaned up when the user navigates away.

def mount(_params, assigns) do
  subscribe(MyApp.PubSub, "room:lobby")
  {:ok, %{messages: []}}
end