Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Client Output and Presentations

This page describes how the web client interprets notify() and present() output, including content types and metadata that affect rendering. These events flow through moor-web-host over FlatBuffers and are rendered with the web client's richer UI.

Rich Content Formats

The web client supports rich output formats and applies strict safety rules to prevent spoofing or unsafe rendering.

Djot

Djot is a modern, structured markup format designed for predictable rendering with fewer edge cases than Markdown. We use it because it supports rich formatting while keeping tighter restrictions that reduce spoofing risks.

Learn more: https://github.com/jgm/djot

HTML

HTML output is supported, but it is heavily sanitized (via DOMPurify) before rendering and presented with a very restricted subset of elements. This allows basic formatting while preventing untrusted content from injecting scripts or spoofing the UI.

Login and Welcome Screen Customization

When a browser connects, the web client invokes the welcome flow through moor-web-host and renders the narrative output from the login command. You can customize this welcome message using rich content types:

  • text/html: sanitized HTML for branded layouts
  • text/djot: djot markup for structured content
  • text/x-uri: a URL rendered in a sandboxed iframe
// Example: in $do_login_command with no args
notify(connection, "= Welcome to the Observatory\n*Please log in to continue.*", false, false, "text/djot");

For iframe welcome content, return a URL as text/x-uri:

notify(connection, "https://example.com/welcome.html", false, false, "text/x-uri");

The iframe is sandboxed and intended for trusted, static content.

notify() Content Types

The notify() builtin can specify a content_type in rich mode. The web client understands:

Content TypeDescription
text/plainPlain text (default)
text/htmlHTML-formatted output (sanitized)
text/djotDjot-formatted output
text/x-uriURL rendered in an iframe
text/tracebackStack trace formatting
// Plain text
notify(player, "You see a lantern.");

// HTML
notify(player, "<strong>Warning:</strong> Low power.", false, false, "text/html");

// Djot
notify(player, "= Status\n*All systems nominal*", false, false, "text/djot");

When sending text/html or text/djot, the web client turns anchors into interactive elements. It recognizes moo:// links for client-side actions:

Link PatternAction
moo://cmd/<command>Run the URL-decoded command as if typed by the player
moo://inspect/<ref>Show object info popover (calls <ref>:inspection)
notify(player, "[look](moo://cmd/look)", false, false, "text/djot");
notify(player, "[examine sword](moo://cmd/examine%20sword)", false, false, "text/djot");

notify() Metadata

The web client reads metadata attached to narrative events. Common keys include:

KeyTypeDescription
presentation_hintstringStyling hint (inset, processing, expired)
group_idstringGroup related lines together
tts_textstringAlternate text for screen readers
thumbnaillist[content_type, binary_data] for preview images
link_previewmapRich link preview data

If your core uses metadata, you can shape how the web client presents or groups output without changing the visible message body.

notify() targeting:

  • notify(player, ...) — sends to all of that player's connections AND writes to the event log (persistent history)
  • notify(connection, ...) — sends only to that specific connection (negative object number), does NOT write to event log or other connections

The event_log() builtin explicitly writes to the event log without displaying anything—useful when you've only notified a specific connection but still want the message recorded in history.

Presentation Hints

presentation_hint guides visual treatment for a line or group:

HintVisual Treatment
insetRender in an inset card (look output, summaries)
processingShow spinner/animation (in-progress operations)
expiredFaded appearance (stale content)
metadata = ["presentation_hint" -> "processing"];
notify(player, "Calibrating sensors...", false, false, "text/plain", metadata);

Grouping with group_id

When consecutive messages share the same presentation_hint and group_id (and the same actor, if provided), the web client visually groups them together. This enables:

  • Multi-line look descriptions shown as a single card
  • Collapse/expand behavior for grouped output
  • Consistent visual treatment across related messages
// Grouped look description
metadata = ["presentation_hint" -> "inset", "group_id" -> "look:#123"];
notify(player, "You are in the Observatory.", false, false, "text/plain", metadata);
notify(player, "A brass telescope points skyward.", false, false, "text/plain", metadata);

Message Staleness

The client automatically marks certain messages as "stale" when superseded. For example, when a player runs look again, the previous look output is visually dimmed and its links become non-interactive. This helps players understand which information is current.

Rewritable Messages

For dynamic content that updates in place (e.g., progress indicators, streaming AI responses), the client supports rewritable messages.

// Initial message with rewritable ID
metadata = [
    "presentation_hint" -> "processing",
    "rewritable_id" -> "task-123",
    "rewritable_owner" -> this,
    "rewritable_ttl" -> 30
];
notify(player, "Processing...", false, false, "text/plain", metadata);

// Later: rewrite the message
metadata = ["rewrite_target" -> "task-123"];
notify(player, "Processing complete!", false, false, "text/plain", metadata);

Rewritable metadata keys:

KeyTypeDescription
rewritable_idstringUnique identifier for the message slot
rewritable_ownerobjectObject that owns this slot (security check)
rewritable_ttlnumberTime-to-live in seconds before expiry
rewritable_fallbackstringContent to show if TTL expires without rewrite
rewrite_targetstringID of the message to rewrite (on the replacement message)

Rich Input Prompts

The web client supports structured input prompts that go beyond simple text input. Use read() with metadata to trigger these. See read() builtin reference.

Input Types

TypeUI ControlUse Case
textSingle-line text fieldNames, short answers
text_areaMulti-line textareaDescriptions, long text
numberNumber inputQuantities, coordinates
choiceButtons or dropdownMultiple choice selection
yes_noYes/No buttonsBinary questions
yes_no_alternativeYes/No/Alternative buttonsWith custom option
yes_no_alternative_allYes/Yes All/No/AlternativeBatch approvals
confirmationOK buttonAcknowledgments
imageFile picker with previewImage uploads
fileFile pickerGeneral file uploads

Input Metadata Fields

FieldUsed ByDescription
input_typeAllType of input control
promptAllPrompt text (supports Djot)
tts_promptAllAccessible prompt for screen readers
placeholdertext, text_area, numberPlaceholder text
defaulttext, text_area, numberDefault value
choiceschoiceList of options
min / maxnumberValue constraints
rowstext_areaNumber of rows
accept_content_typesimage, fileAllowed MIME types
max_file_sizeimage, fileMaximum file size in bytes
alternative_labelyes_no_alternative*Label for alternative input
alternative_placeholderyes_no_alternative*Placeholder for alternative

Examples

// Simple text input
read(player, ["input_type" -> "text", "prompt" -> "What is your name?"]);

// Number with constraints
read(player, [
    "input_type" -> "number",
    "prompt" -> "How many items?",
    "min" -> 1,
    "max" -> 100,
    "default" -> 10
]);

// Multiple choice
read(player, [
    "input_type" -> "choice",
    "prompt" -> "Choose a direction:",
    "choices" -> {"North", "South", "East", "West"}
]);

// Yes/No with alternative (for AI agent approvals)
read(player, [
    "input_type" -> "yes_no_alternative",
    "prompt" -> "Apply this change?\n```moo\nplayer.score = 100;\n```",
    "alternative_label" -> "Suggest a different approach:"
]);

// Image upload
read(player, [
    "input_type" -> "image",
    "prompt" -> "Upload your profile picture:",
    "accept_content_types" -> {"image/png", "image/jpeg", "image/gif"},
    "max_file_size" -> 1048576  // 1MB
]);

present() Targets and Attributes

The present() builtin is used to open or update panels and windows. See Presentations for comprehensive documentation.

Quick reference:

// Open a verb editor
present(player, "edit-look", "text/plain", "verb-editor", "",
        {{"object", "#123"}, {"verb", "look"}, {"title", "Edit look"}});

// Show profile setup dialog
present(player, "profile-setup", "text/plain", "profile-setup", "",
        {{"title", "Set up your profile"}, {"fields", "pronouns,description"}});

// Dismiss a presentation
present(player, "edit-look");