-
Notifications
You must be signed in to change notification settings - Fork 0
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 |
<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.
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).
<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
|
<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.
<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.
<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 }.
<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.
SpeechRecognitionin most browsers sends audio to a vendor service. It is not local, which matters if the rest of your surface is.
<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".
<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.
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.
{
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.