Interactions

An interaction is what one part of a page lets someone do: its state and its actions, defined once for people and agents.

Define it in a component's browser code and bind the visible controls to its actions. People, Player shortcuts and browser agents (WebMCP) then use the same operations, with the same validation and availability.

Define an interaction

This component client keeps a count. It defines a counter interaction over its root element, reports the count as its context, and offers two actions.

import type { ComponentClientContext } from "@pathmx/core"
import { defineInteraction } from "@pathmx/core/interactions"
import { z } from "zod"

export default function mount({ element }: ComponentClientContext<undefined>) {
  const output = element.querySelector("output")!
  let count = 0
  const show = () => {
    output.value = String(count)
    counter.refresh()
  }
  const counter = defineInteraction(element, {
    kind: "counter",
    read: () => ({ count }),
    actions: {
      increment: {
        title: "Add one",
        state: () => ({ available: true, pending: false }),
        invoke: () => {
          count++
          show()
        },
      },
      set: {
        title: "Set the count",
        description: "Replace the count with a whole number.",
        input: z.object({ value: z.number().int() }),
        state: () => ({ available: true, pending: false }),
        invoke: ({ value }) => {
          count = value
          show()
        },
      },
    },
  })
  counter.bind(element.querySelector("button")!, { action: "increment" })
  return { dispose: () => counter.dispose() }
}
  • kind names what the interaction is; read() returns the context a reader sees.
  • Each action has a title, an optional description, an optional Zod input, a state() with available, pending and an optional reason, and invoke(). Invocation validates input against the schema and refuses an unavailable action.
  • Call refresh() whenever the context or an action's state changes, so discovery and bound controls update.
  • dispose() removes the interaction and its bindings.

Bind controls

bind(element, { action }) declares which control performs an action. A bound <button> invokes its action when clicked, so don't add a click listener of your own: the action would run twice. A submit button inside a form keeps the form's native submission.

OptionMeaning
actionThe action the control performs.
inputFor an action with input: a function returning { value } from the control, or undefined while it isn't ready.
scope"local" (default) matches the control itself; "interaction" also matches anything inside the interaction's members.
presentReturns { visible, label } when the control's own text isn't its label.

Pass members: () => elements in the definition when the interaction spans elements outside its root, such as fields an editor makes editable elsewhere on the page.

Discover and invoke

interactionRegistry lists the interactions within some part of the document. Each result has the interaction's surface (its kind, context, and each action's title, description, input as JSON Schema and state), a handle to read and invoke it, and its bindings.

import { interactionRegistry } from "@pathmx/core/interactions"

const [counter] = interactionRegistry
  .discover({ within: [document.body] })
  .filter(({ surface }) => surface.kind === "counter")
counter?.surface.context // { count: 0 }
await counter?.handle.invoke("set", { value: 3 })

const stop = interactionRegistry.subscribe({ within: [document.body] }, (found) => {
  // Called when interactions appear, disappear or change.
})

An invocation that can't run rejects with an InteractionInvocationError whose code is unavailable, busy, invalid-input (with the schema's issues), disposed or aborted.