Skip to content

Events

Chat uses two distinct event channels:

  1. Bus ops — actor mailbox messages over NATS. See actor ops.
  2. DOM CustomEvents — UI signals that bubble up the component tree. This page documents those.

All Chat CustomEvents use bubbles: true, composed: true.

From <chat-app>

Class: MatrixChatApp. Declared static emits:

EventDetailWhen
app.connected(none)Bus connection established.
app.disconnected(none)Bus connection lost.
app.message-sent(none)A user message has been sent.
app.stream-started(none)Streaming begins for the current prompt.
app.stream-ended(none)Streaming completes (any phase: done/error/cancelled).

These are pure status signals; they do not carry payloads.

From <conversation-surface>

Class: ConversationSurfaceActor. Declared static emits:

EventDetailWhen
conversation:target-changed{ mount }The target actor was set or replaced.
conversation:session-changed{ sessionId }Session ID was assigned (after first prompt) or resumed.
conversation:error{ message }Surface-level error (rare; typically render errors).

From <conversation-input>

Internal; bubbles through the surface root:

EventDetailWhen
conversation:submit{ text }User pressed Enter.

The surface listens at the shadow root and converts to a _handleSubmit(text) call.

From <conversation-session-list>

EventDetailWhen
conversation:session-selected{ sessionId }User clicked a session entry.
conversation:session-new(none)User clicked "New session".
conversation:sessions-refresh-requested(none)User clicked refresh.

The surface listens for these, calls _resumeSession, _handleNewSession, or _fetchSessionList, and dispatches state-changed events upward in turn.

From <chat-input>, <chat-message-list>, <chat-sidebar>, etc. (chat-app)

These are internal to the chat-app shell. The chat app itself has no canonical "external API" event vocabulary — its consumers are tightly coupled to its layout. The five app.* events above are the documented external surface.

If you embed chat-app inside another app, listen for those five events and ignore the internal traffic.

Capability prompt events

MatrixChatApp participates in the capability-realm flow:

EventDetail (subset)When
(capability prompt){ requestId, component, domain, requestedRight, mount, action }The chat security realm wants user approval.
(capability decision){ requestId, decision, approved, denied, ... }User answered (approve/deny).

These are dispatched by ChatSecurityRealmService and consumed by MatrixChatApp to render the prompt and forward the decision back.

Event listening in host shells

If you embed <chat-app>:

ts
chatApp.addEventListener('app.stream-started', () => updateUiBusyIndicator(true));
chatApp.addEventListener('app.stream-ended',   () => updateUiBusyIndicator(false));

If you embed <conversation-surface>:

ts
surface.addEventListener('conversation:session-changed', (e) => {
  const sessionId = e.detail.sessionId;
  persistSessionLocally(sessionId);
});

composed: true means the event escapes shadow DOM, so listening at the host (parent of <conversation-surface>) works without piercing the shadow root.

Events that do NOT bubble out

A number of internal events are stopped at the surface boundary:

  • The renderer's own internal events (e.g., scroll/expand) stay inside the renderer's shadow DOM.
  • The session-list's internal click events are translated into the documented vocabulary.

If you need an event that's not in the documented list, the Chat package likely doesn't expose it, and embedding a different element directly is the wrong layer.

See also

Source: projects/matrix-3/packages/chat/src/ChatApp.ts:184-190 (app emits), chat-component/src/surface/ConversationSurfaceActor.ts:34-38 (surface emits), individual surface element classes.