Skip to content

Component Reference

neodigm edited this page Oct 4, 2026 · 1 revision

Component Reference

Nine elements. All tags are prefixed machvive-chat-syncopation.

Tag Subpath Renders
-services /services nothing (headless)
machvive-chat-syncopation /container layout
-canvas /canvas the transcript
-prompt /prompt the composer
-nudge /nudge suggestion chips
-cli /cli a command line
-voice /voice microphone and read-aloud
-inspector /inspector bus traffic
-history /history stored conversations

Services

<machvive-chat-syncopation-services> — headless. Owns the bus, the conversation, the daemon and the cache. Everything else finds it by walking up the DOM.

Attributes: transport, model, endpoint, persist, max-turns, streaming — see Getting Started.

Properties

Property Type Notes
bus PubSub See Services and the Bus
conversation Conversation The record list; the only thing allowed to mutate it
daemon Daemon Generation. daemon.busy, daemon.stop()
cache Cache IndexedDB-backed storage
config object The resolved configuration

Methods: send(text) → Promise<record>. Convenience for page code; components use the daemon.

Events: services-ready (bubbles, composed) once it has booted.


Container

machvive-chat-syncopation — layout only. Creates its own services element, in its light DOM, when none governs it, passing through any config attributes set on it.

Slots: header, default, footer.

CSS: --mcs-height (default 32rem). A chat surface needs a bounded height or the transcript grows the page and the composer walks off the bottom of the viewport.

Properties: services, conversation. Methods: send(text).


Canvas

<machvive-chat-syncopation-canvas> — the transcript. An <ol role="log" aria-live="polite"> of bubbles, styled by role and status.

It is append-only: a streaming turn updates one node's text rather than re-rendering the list, and autoscroll follows the bottom only when the reader is already within 48px of it. Both are deliberate.

Record text is rendered with textContent, always. Model output is untrusted input.

Styling hooks (inside the shadow root, so restyle via tokens):

Selector Applies to
li[data-role="user"] the user's turns, right-aligned
li[data-role="assistant"] replies
li[data-role="system"], li[data-role="tool"] muted, monospace
li[data-status="pending"] shows a blinking caret (suppressed under prefers-reduced-motion)
li[data-status="error"] outlined in --mcs-danger

Prompt

<machvive-chat-syncopation-prompt> — the composer. A textarea that grows to 8rem and a button that is Send, or Stop while a turn is generating.

Attributes: placeholder, label (the accessible name; visually hidden).

Keys: Enter sends. Shift+Enter inserts a newline. IME composition is respected, so Enter mid-composition does not send.

Methods: fill(text, { send = false }) — puts text in the field and focuses it.

Events: prompt-submit with { text } (bubbles, composed).

Listens for: prompt:fill on the bus, which is how the nudge, the CLI and voice hand text to the composer without sending it.


Nudge

<machvive-chat-syncopation-nudge> — suggested openings, and a quiet re-engagement prompt.

Attributes

Attribute Meaning
suggestions Pipe-delimited: "One|Two|Three"
idle-ms Milliseconds of silence before the idle prompt. Omit to disable
idle-text What the idle prompt says

Chips clear once the user says anything — the conversation is then the context, and stale chips only take up space. The idle prompt fires once per conversation, never on a loop.

Properties: suggestions (parsed array). Events: nudge-select with { text }; nudge-idle.

Selecting a chip fills the composer. It does not send.


CLI

<machvive-chat-syncopation-cli> — a keyboard-first surface. Anything not starting with / is sent as an ordinary message, so this can replace the composer outright.

Built-in commands: /help, /clear, /transport [name], /stop.

Slash commands run locally and never reach the model. Up and Down walk the session history, which is held in memory only — a command history that outlived the tab would be a record of what someone typed that they never asked anyone to keep.

Methods

cli.register('order', {
  describe: 'look up an order by number',
  run: (args) => `looking up ${args}…`
});

Register domain verbs rather than forking the component. A command that throws is reported in the output line; it does not take the surface down.

Events: cli-command with { name, args }.


Voice

<machvive-chat-syncopation-voice> — push-to-talk dictation and read-aloud, both capability-gated. What the browser cannot do is disabled with the reason shown, rather than rendered as a button that silently does nothing.

Attributes: lang (defaults to the config locale, then navigator.language).

Methods: start(), stop(). Events: voice-transcript with { text }.

Dictation fills the composer. It never sends — a microphone that transmits the moment it recognises a phrase will eventually send half a sentence, or a conversation happening in the room. Read-aloud speaks completed turns only.

Privacy. SpeechRecognition in most browsers sends audio to a vendor service. It is not local, which matters if the rest of your surface is.


Inspector

<machvive-chat-syncopation-inspector> — live bus traffic, for whoever is building the surface. Subscribes to the wildcard topic, so it shows events from components that did not exist when it was written, including your own.

Attributes: limit (default 200 events). Properties: events — [{ at, topic, detail }]. CSS: --mcs-inspector-height (default 14rem).

Pause and Clear are in its header. Payloads are rendered as text — a dev tool is not exempt from the rule about untrusted model output.

It is a development tool, not telemetry: nothing leaves the page, and importing it emits nothing. If no services element governs it, it says so — which is the fastest answer to "why is my chat component not receiving anything".


History

<machvive-chat-syncopation-history> — stored conversations, and the controls to take them back. See Data Agency.

Methods

Method Does
refresh() Re-read the cache
export(id?) Serialise to JSON and offer it as a download; returns the JSON
forget(id) Delete one conversation
forgetAll() Delete all of them

Events: history-export with { json }; history-forget with { id } (null for all); history-open with { id }.

CSS: --mcs-history-height (default 14rem).

Opening a conversation replays it into the live one, so the canvas shows it. Deleting removes the stored row and the in-memory copy. When persistence is off it says so rather than appearing broken.


The service layer, directly

Everything is also importable without any element:

import {
  PubSub, Conversation, Daemon, Cache,
  createRecord, ROLES, isPending,
  META, annotate, readMeta, userMeta,
  resolveConfig, DEFAULTS, transports,
  THEME_CSS, CONTROL_CSS,
  findServices, whenServices, SERVICES_TAG
} from '@machfivetechchicago/machvive-chat-syncopation-ai';

Useful when you are writing your own surface and want the state layer without the rendering. findServices(node) and whenServices(node) are what the components use; whenServices resolves null on timeout rather than hanging, because upgrade order is not something a component can rely on.

The record shape

{
  id: 'm-k3j2h1-0',
  role: 'user',               // user | assistant | system | tool
  text: 'Where is my order?',
  at: '2026-10-04T14:12:03.000Z',
  status: 'complete',         // complete | pending | error
  meta: {}                    // open; reserved keys are namespaced mcs:
}

A record is data, never a DOM node — the same turn is rendered by the canvas, replayed by history and read by the inspector. Reserved meta keys (META.TOKENS, META.LATENCY, META.MODEL, META.SOURCE, META.ERROR, META.CITATIONS, META.TOOL_CALLS) are namespaced because meta is deliberately open; your own keys are carried verbatim and never stripped. userMeta(record) returns just yours.

Clone this wiki locally