Asking the visitor something

The tablet can put a question over a dimmed screen — the shipped one is a five-star rating after a conversation ends. apps/tablet/src/prompts.ts contains the entire implementation and is the file to edit or delete. A prompt is a name plus three answers: when it opens, how long it stays, and what it draws, and all three live there. Nothing about it reaches the plugin, the wire format, or the component library. Thus, a partner replacing the rating with a form, a poster, or a 3D viewer changes only one file in their own app.

A prompt defines when it opens as a predicate over two consecutive snapshots. Two openers ship, and they compose — which is the interesting part:

/* The tablet noticed something: a conversation ended and the kiosk went idle. */
opensOn: afterConversation

/* The host asked: a Blueprint calling Set Tablet State String("prompt", "poster"). */
opensOn: whenHostAsks('poster')

/* Both - what the shipped rating uses. */
opensOn: (previous, next) =>
    afterConversation(previous, next) && next.state.prompt === 'feedback',

The second is the general host→tablet route and requires nothing new: prompt is an ordinary state key, like subtitles.

Prefer the third shape where it fits. On its own, whenHostAsks watches the key’s own edge, bringing two obligations. First, the host’s state store compares before storing, so re-publishing the same string emits no patch; the prompt opens exactly once until the key is cleared. Second, a key left published is a question the next tablet to connect will find and ask, because hello carries a complete snapshot. ANDing it with something the tablet can see for itself removes both. The rating is armed by sessionActive going false, so prompt may sit at "feedback" for the life of the process, and every visitor is still asked.

Thus, the entire Unreal side of the shipped rating is one node that is never undone. It belongs on BeginPlay alongside the existing subtitles and assistancePending publishes:

BeginPlay  ->  ...  ->  Set Tablet State String ("prompt", "feedback")

Publish a different name, and a different card appears; publish nothing, and none does. This is the entire host-side vocabulary for this, and it uses a node you already had.

Placing it after Disconnect works identically — the sessionActive edge always reaches the tablet in a later patch than a value the graph wrote synchronously — but it buys one thing BeginPlay cannot: publishing per disconnect path. Thus, a failed conversation can be excluded from the question, while a clean Stop still asks. Note that Disconnect in the reference graph is shared by ten callers, so the shared tail asks after all of them.

The answer travels back via the ordinary command channel: answer('4') sends feedback with the value "4", landing on On Tablet Command like any button press. The graph can then use or ignore it. A prompt requiring a reply does not justify a new message type.

Where to send feedback off the box is marked in prompts.ts as Deployment hook — a fetch in the renderer that collected the score. Alternatively, posting from the graph is usually the better approach: it keeps the endpoint and any API key off a tablet sitting on a guest network, and the score is already there.

The card closes on any of five events, and only the first is an answer: the visitor responds, they tap anywhere off the card, the renderer’s timeoutMs runs out (20 s for the rating), a conversation starts, or the host becomes unreachable. None of them needs wiring.