Message Tag Interceptors¶
Extensions can intercept custom XML-like tags embedded in chat messages. This lets you build inline interactive elements — for example, a <spotify-search query="..."> tag that triggers a search when rendered.
Declare the backend capability¶
If the extension has a backend entry, declare its use of message-tag interception during backend startup:
const releaseTagCapability = spindle.frontendCapabilities.declare(
'message_tag_interceptor'
)
The host includes this runtime declaration in frontend bootstrap state. Chat content then remains in its initial hidden state until the extension frontend attaches at least one tag interceptor (or the bounded host fallback expires), preventing raw tag payloads from flashing during cold chat load. The returned function retracts the declaration; worker shutdown also clears it automatically.
This declaration does not define a tag or replace the frontend registration.
The frontend remains authoritative for tagName, attrs,
removeFromMessage, and the handler, so those options can still come from
per-user settings.
ctx.messages.registerTagInterceptor(options, handler)¶
Register a handler that fires whenever a matching tag appears in a rendered message. Returns an unsubscribe function.
const unsub = ctx.messages.registerTagInterceptor(
{ tagName: 'my-action' },
(payload) => {
console.log('Tag found:', payload.tagName, payload.attrs, payload.content)
console.log('In message:', payload.messageId, 'chat:', payload.chatId)
}
)
// Unsubscribe later
unsub()
SpindleMessageTagInterceptorOptions¶
| Field | Type | Description |
|---|---|---|
tagName |
string |
The tag name to match (e.g. 'my-action' matches <my-action>) |
attrs |
Record<string, string> |
Optional. Only match tags that have these exact attribute values |
removeFromMessage |
boolean |
Optional. If true, the matched tag is stripped from the rendered message output. During streaming, content is hidden as soon as the matching opening tag appears and replaced with a subtle inline "extension is processing" indicator. Normal rendering resumes after the closing tag arrives. |
SpindleMessageTagIntercept (handler payload)¶
| Field | Type | Description |
|---|---|---|
extensionId |
string |
Your extension's ID |
tagName |
string |
The matched tag name |
attrs |
Record<string, string> |
All attributes on the tag |
content |
string |
Inner text content of the tag |
fullMatch |
string |
The full matched tag string |
messageId |
string? |
ID of the message containing the tag |
chatId |
string? |
ID of the chat containing the message |
isUser |
boolean? |
Whether the message is from the user |
isStreaming |
boolean? |
Whether the message is still streaming |
Example: Inline Search Tag¶
// Intercept <spotify-search query="..."> tags in messages
const unsub = ctx.messages.registerTagInterceptor(
{ tagName: 'spotify-search' },
(payload) => {
const query = payload.attrs.query
if (query) {
ctx.sendToBackend({ type: 'search', query })
}
}
)
When an LLM or user includes <spotify-search query="chill vibes"> in a message, the handler fires and can trigger extension logic.