AI streaming
Render arriving Markdown as one message and handle completion, cancellation and failure.
An AI response usually arrives in several pieces. The application receives network data and accumulates text; QuireMarkdown renders the Markdown received so far. It does not send model requests or determine when they finish.
1. Pass the message to the reader
Keep the component imports from Quick start. message.id is a stable identifier for one response, and message.text is the accumulated source:
<QuireMarkdown
:content="message.text"
:identity="message.id"
:status="message.receiving ? 'receiving' : 'complete'"
@error="reportReadingError"
/>
Pass the complete received source on each update. Prefix appends reuse a parsing session;
replacement, clearing, identity changes and replacing the options object create a fresh
session and release old resources. status is receiving, complete, stopped or error.
It describes transport state; stopping retains text and neither freezes blocks nor adds
synthetic closing delimiters. Late link and footnote definitions can update earlier content.
The application cancels its own network request.
2. Try appending and stopping
Choose “Start streaming” and watch headings, links and footnotes arrive. “Stop” retains received text. The application remains responsible for cancelling its network request.
Server-rendered first content
SSR produces semantic headings, lists and links by default, with the same parsing strategy
on the first client render. Resource loading starts only after client mount. To defer initial
parsing, use ssr="source": the server and initial client frame show escaped source, then
parse after mounting. An existing snapshot may be passed as state, mutually exclusive
with content.
3. Receive a real text response
This reusable function handles HTTP bodies containing raw Markdown text. Pass the application's AbortSignal to both fetch and this function; update message.text in onText. Set receiving on start, complete on success, stopped on user cancellation and error for other failures. Guard callbacks with the request identity so an old request cannot overwrite a different message. The host aborts on unmount.
recipes/response.ts only reads and decodes the response body. readMarkdownResponse(response, signal, onText) supplies accumulated source to onText and returns the complete text when finished. The application owns its message list, request state and error feedback.
/**
* Consume an HTTP response whose body is raw Markdown text, not SSE or provider JSON.
* @param response - Response returned by the application's fetch call.
* @param signal - The same AbortSignal passed to fetch by the host.
* @param onText - Receives the entire accepted Markdown prefix after each decoded chunk.
* @returns Complete source, preserving UTF-8 characters split across transport chunks.
*/
export async function readMarkdownResponse(response: Response, signal: AbortSignal, onText: (text: string) => void): Promise<string> {
signal.throwIfAborted();
if (!response.ok || !response.body) throw new Error(`markdown-response:${response.status}`);
const reader = response.body.getReader(), decoder = new TextDecoder();
let content = "";
/** Cancel producer reads; the component retains whatever the host has already accepted. */
const cancel = (): void => { void reader.cancel().catch(() => {}); };
signal.addEventListener("abort", cancel, { once: true });
try {
while (true) {
const chunk = await reader.read();
signal.throwIfAborted();
if (chunk.done) break;
content += decoder.decode(chunk.value, { stream: true });
onText(content);
}
const tail = decoder.decode();
if (tail) { content += tail; onText(content); }
return content;
} finally {
signal.removeEventListener("abort", cancel);
await reader.cancel().catch(() => {});
reader.releaseLock();
}
}
For SSE or JSON events, decode the protocol in the host first and append only text deltas.
Do not feed data:, event JSON or binary chunk boundaries directly to Markdown. TextDecoder must preserve decoding state between chunks.