Skip to main content

AiStreamContainer

Package: @synapse-ui/stream-container
Selector: synapse-ai-stream-container
Status: Stable

A container built for rendering streaming markdown chunks with syntax highlighting and copy-to-clipboard utilities.

Overview

AiStreamContainer accepts incremental text chunks from a generic SSE stream, renders them as markdown using marked, and applies Shiki syntax highlighting to fenced code blocks. It is optimized for high-frequency DOM updates with OnPush change detection.

Installation

npm install @synapse-ui/stream-container @synapse-ui/theme @synapse-ui/core
npm install marked shiki

Basic Example

import { Component, signal } from '@angular/core';
import { AiStreamContainerComponent } from '@synapse-ui/stream-container';

@Component({
selector: 'app-response',
standalone: true,
imports: [AiStreamContainerComponent],
template: ` <synapse-ai-stream-container [content]="streamContent()" [streaming]="isStreaming()" [autoScroll]="true" /> `,
})
export class ResponseComponent {
streamContent = signal('');
isStreaming = signal(false);

connectToSSE(url: string): void {
this.isStreaming.set(true);
const source = new EventSource(url);

source.onmessage = (event) => {
this.streamContent.update((prev) => prev + event.data);
};

source.onerror = () => {
source.close();
this.isStreaming.set(false);
};
}
}

API Reference (Planned)

Inputs

InputTypeDefaultDescription
contentstring''Accumulated markdown content
streamingbooleanfalseShows cursor indicator when true
autoScrollbooleantrueScroll to bottom on new chunks
highlightThemestring'github-dark'Shiki theme for code blocks
allowedTagsstring[]Safe defaultsHTML tags allowed after sanitization
emptyMessagestring'Waiting for response...'Shown when content is empty

Outputs

OutputPayloadDescription
codeCopied{ language: string; code: string }Fired when user copies a code block
renderCompletevoidFired when streaming ends and final render completes

Streaming Architecture

flowchart LR
SSE[SSE Chunks] --> Buffer[Chunk Buffer]
Buffer --> Marked[marked parser]
Marked --> Sanitize[DOMPurify / sanitizer]
Sanitize --> DOM[Incremental DOM update]
Marked -->|fenced code| Shiki[Shiki highlighter]
Shiki --> DOM

Chunk Buffer Strategy

The container maintains an internal buffer to avoid re-parsing the entire document on every chunk:

  1. Append new chunk to buffer.
  2. Debounce parse (configurable, default ~16ms for 60fps).
  3. Diff and patch DOM where possible.
  4. Highlight only new or changed code blocks.

See ADR 004 for the full decision record.

Markdown Support

Supported via marked with GFM (GitHub Flavored Markdown):

  • Headings, paragraphs, lists
  • Bold, italic, strikethrough
  • Fenced code blocks with language tags
  • Blockquotes
  • Tables (GFM)
  • Links (sanitized, target="_blank" with rel="noopener")

Unsupported or stripped for security: raw HTML, script tags, iframes.

Code Block Features

Each fenced code block renders with:

  • Language label badge
  • Copy-to-clipboard button
  • Shiki syntax highlighting
  • Horizontal scroll for long lines
/* Uses these tokens */
background: var(--synapse-ai-code-bg);
font-family: var(--synapse-font-family-mono);

SSE Integration Pattern

Synapse UI does not manage the SSE connection — your app does. Recommended pattern:

import { inject, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { createSSEObservable } from '@synapse-ui/core';

// Planned utility in @synapse-ui/core
createSSEObservable('/api/chat')
.pipe(takeUntilDestroyed())
.subscribe({
next: (chunk) => this.content.update((c) => c + chunk),
complete: () => this.streaming.set(false),
});

Accessibility

  • Container uses role="log" with aria-live="polite" during streaming.
  • Copy buttons are keyboard accessible with aria-label="Copy code".
  • Streaming cursor is decorative (aria-hidden="true").
  • Code blocks are navigable via keyboard (Tab to copy button).

Performance Guidelines

ScenarioRecommendation
Chunks < 50/secDefault settings
Chunks > 50/secIncrease debounce to 32–50ms
Very long responsesEnable CDK virtual scroll (post-MVP)
Multiple streamsUse separate container instances

Theming

ElementToken
Container background--synapse-surface-primary
Text--synapse-text-primary
Code block background--synapse-ai-code-bg
Stream cursor--synapse-ai-stream-cursor
Blockquote border--synapse-border-strong