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.
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() }
}
kindnames what the interaction is;read()returns the context a reader sees.- Each action has a
title, an optionaldescription, an optional Zodinput, astate()withavailable,pendingand an optionalreason, andinvoke(). 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(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.
| Option | Meaning |
|---|---|
action | The action the control performs. |
input | For 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. |
present | Returns { 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.
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.