inversedocs
Pages

Interface kit

Let your app notice how people use it: who is looking, what they say, how they react. Inside an Inverse room, that context goes to the room with each person's name on it, and on to your agent.

Start

The kit uses native browser APIs and has no dependencies. Start it from a user gesture, because it asks for the camera and microphone.

app.js
import { createInterfaceKit } from "inverse-embed/interface-kit";

button.onclick = async () => {
  const kit = await createInterfaceKit({ preset: "listen", room: true }).start();
  kit.on("voice", (e) => console.log(e.data));               // your own speech, interim and final
  kit.on("transcript", (line) => console.log(line.by, line.text)); // everyone in the room
  kit.on("reaction", (e) => console.log(e.data.emoji));
};

kit.stop() releases the camera and microphone. Outside an Inverse frame the kit still works; your callbacks get every event and nothing is sent anywhere.

Presets

PresetTurns on
listenVoice and attention.
seeFaces and attention.
allBoth. The default when you pass no perception options.

Faces turn on only where the browser has FaceDetector, so people aren't asked for a camera that can't be used. Voice uses the browser's speech recognition; where it's missing you get one { supported: false } event.

Options

every option
const kit = createInterfaceKit({
  preset: "all",
  video: document.querySelector("video"), // or true
  faces: true,              // native FaceDetector, about twice a second
  attention: true,          // { visible, focused, looking }
  voice: { lang: "en-US" },
  commands: { "next slide": () => next(), "go back": () => back() },
  room: true,               // receive everyone's attributed lines
  pens: true,               // turn on the room's markup tools (off by default)
  share: true,              // send derived context to the room (default)
  onContext: (event) => console.log(event.kind, event.data),
});

Events

Every event is { kind, data, at }. Listen with kit.on(kind, fn), which returns an unsubscribe function, or with onContext for all of them.

Kinddata
presence{ present }
faces{ count, boxes }, or { supported: false }
attention{ visible, focused, looking }
voice{ text, final }, or { supported: false }
command{ name, text } when a final transcript contains a phrase from commands
reaction{ emoji?, expression?, intensity? }
customAnything you send with kit.emit("custom", data)

Reactions

Call kit.react("๐Ÿ”ฅ"), or let voice cues react for you: โ€œhahaโ€ ๐Ÿ˜‚, โ€œwowโ€ ๐Ÿ˜ฎ, โ€œniceโ€ ๐Ÿ‘, โ€œughโ€ ๐Ÿ˜ฉ. Browsers have no facial expression API, so faces don't trigger reactions yet.

Attribution

Inverse stamps every event that reaches the room with by: the name of the room member it came from. The host sets it, not your app, so an app can't speak for someone else.

Room lines

With room: true, while the kit is started, on("room") receives everyone's lines and on("transcript") only the spoken ones. Nothing flows until the participant allows it: Inverse asks once per app, inline.

RoomLine
{ kind: "voice" | "reaction", by: "Ana", text: "can we zoom in?", at: 1793491200000 }

Markup tools

An Inverse room is just your app and a bottom bar. The markup tools (pen, highlighter, arrow, box, eraser) are off unless you turn them on, either with your app's setting (inverse share --pens or the toggle in /dev) or from the kit with pens: true. The kit sends it when Inverse says hello, before start(). When they're on, a pen button in the bottom bar opens the tools, and everyone's marks show over the app.

Streams

kit.stream(name, { mirror }) is a shared pool of small JSON values: votes, picks, ratings. Inverse stamps each push with who sent it and keeps the latest 500 in the room. Names match [a-z0-9-]{1,40}; values are up to 4 KB, at most 20 pushes a second.

vote.js
const votes = kit.stream("vote", { mirror: "latest" });
votes.on((entries) => render(entries));      // [{ name, by, value, at }]
button.onclick = () => votes.push({ pick: 2 });
votes.values();                              // the current mirror
mirrorWhat comes back into each frame
allEvery entry (latest 500)
latestThe latest value per person
noneNothing: the stream is for agents only

Values mirror back while the kit is started, and only after the participant allows room data (the room grant). Agents read streams in the intent package by alias, and through the MCP tool get_stream.

Cursors

kit.cursors({ target, render }) shares the pointer, at most 20 times a second, normalized to target (or the viewport). Each update carries an intent and over: the closest element's aria-label, #id or tag. Positions travel through the room's presence and are never stored. It needs the participant's cursors grant.

cursors.js
const cursors = kit.cursors({ target: document.querySelector("main"), render: true });
cursors.on((peers) => console.log(peers)); // [{ id, name, color, x, y, intent, over }]
cursors.intent("drag", "Card: Launch plan");
cursors.stop();                            // stops sharing, removes the drawn cursors

render: true draws a small cursor and name pill in each person's colour inside target; it ignores the pointer and skips animation under reduced motion. Agents never get positions: a pause of 1.5 seconds or more on one element becomes an attention event { intent, over, dwellMs }.

What leaves the browser

Your callbacks see everything. The room gets only derived context:

  • Never video, audio or images.
  • Final transcripts only; interim text stays in the app.
  • Face counts without the boxes.
  • At most 4 events a second per kind, each up to 4 KB.
  • The room keeps the latest 500 context events.
  • Nothing at all with share: false.

inverse sync exports the context (context in session.json and events.jsonl, counts in recap.md), and it feeds the intent package. Apps hosted on Inverse can ask for the camera and microphone; the browser still asks each person first.

Without the package

The kit sends one message type to the Inverse frame. You can send it yourself from any framework.

postMessage
window.parent.postMessage(
  { type: "inverse.interface.context", event: { kind: "custom", data: { step: "checkout" }, at: Date.now() } },
  hostOrigin,
);

Send { type: "inverse.interface.hello" } to the parent to learn hostOrigin: the host answers with inverse.hostReady from its origin.