Claude-skill-registry composable-svelte-chat
Streaming chat and collaborative features for Composable Svelte. Use when implementing LLM chat interfaces, real-time messaging, or collaborative features. Covers StreamingChat (transport-agnostic), presence tracking, typing indicators, and WebSocket integration from @composable-svelte/chat package.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/composable-svelte-chat" ~/.claude/skills/majiayu000-claude-skill-registry-composable-svelte-chat && rm -rf "$T"
skills/data/composable-svelte-chat/SKILL.mdComposable Svelte Chat Package
Streaming chat with collaborative features for LLM interactions and real-time messaging.
PACKAGE OVERVIEW
Package:
@composable-svelte/chat
Purpose: Transport-agnostic streaming chat designed for LLM interactions with collaborative features.
Technology Stack:
- Markdown: Rendering with syntax highlighting (Prism.js)
- WebSocket: Real-time communication and presence
- MediaRecorder: Optional voice input integration
- PDF.js: PDF attachment preview
Core Components:
- Transport-agnostic streaming chatStreamingChat
- Presence, typing, cursorsCollaborative Features
- Connection managementWebSocket Manager
State Management: All components follow Composable Architecture patterns with dedicated reducers and type-safe actions.
STREAMING CHAT
Purpose: Transport-agnostic streaming chat for LLM interactions (OpenAI, Anthropic, Ollama, etc.).
Quick Start
import { createStore } from '@composable-svelte/core'; import { StandardStreamingChat, streamingChatReducer, createInitialStreamingChatState } from '@composable-svelte/chat'; // Create chat store const chatStore = createStore({ initialState: createInitialStreamingChatState(), reducer: streamingChatReducer, dependencies: { sendMessage: async function*(message, signal) { // Send to your LLM API (OpenAI, Anthropic, Ollama, etc.) const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message }), signal }); const reader = response.body!.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n').filter(Boolean); for (const line of lines) { if (line.startsWith('data: ')) { const data = JSON.parse(line.slice(6)); if (data.content) { yield data.content; } } } } } } }); <StandardStreamingChat {chatStore} />
Component Variants
MinimalStreamingChat:
- Message list + input
- No toolbar, no status indicators
- Best for embedded chat
StandardStreamingChat (recommended):
- Message list + input
- Toolbar with clear history
- Streaming status indicator
- Best for most use cases
FullStreamingChat:
- Standard features
- Message reactions
- Attachments (images, PDFs, audio)
- Voice input
- Copy/edit/delete messages
- Best for feature-rich chat apps
StreamingChat (legacy):
- Original component
- Use one of the variants above instead
Props
All variants accept:
- Chat store (required)chatStore: Store<StreamingChatState, StreamingChatAction>
FullStreamingChat additional props:
- Enable reactions (default: true)showReactions: boolean
- Enable attachments (default: true)showAttachments: boolean
- Enable voice input (default: true)showVoiceInput: boolean
State Interface
interface StreamingChatState { // Messages messages: Message[]; streamingMessage: string | null; // Current streaming content isStreaming: boolean; // Input inputValue: string; inputDisabled: boolean; // Attachments attachments: MessageAttachment[]; isUploadingAttachment: boolean; // UI State isLoading: boolean; error: string | null; // Voice (if enabled) isRecordingVoice: boolean; voiceTranscript: string | null; } interface Message { id: string; role: 'user' | 'assistant' | 'system'; content: string; timestamp: number; attachments?: MessageAttachment[]; reactions?: MessageReaction[]; isEdited?: boolean; isDeleted?: boolean; } interface MessageAttachment { id: string; type: 'image' | 'pdf' | 'audio' | 'file'; url: string; name: string; size: number; metadata?: AttachmentMetadata; }
Actions
type StreamingChatAction = // Messaging | { type: 'sendMessage'; content: string; attachments?: MessageAttachment[] } | { type: 'streamingStarted' } | { type: 'streamingChunk'; chunk: string } | { type: 'streamingCompleted' } | { type: 'streamingError'; error: string } | { type: 'cancelStreaming' } // Input | { type: 'inputChanged'; value: string } | { type: 'clearInput' } // Messages | { type: 'editMessage'; messageId: string; newContent: string } | { type: 'deleteMessage'; messageId: string } | { type: 'clearHistory' } // Reactions | { type: 'addReaction'; messageId: string; emoji: string } | { type: 'removeReaction'; messageId: string; reactionId: string } // Attachments | { type: 'addAttachment'; attachment: MessageAttachment } | { type: 'removeAttachment'; attachmentId: string } | { type: 'uploadAttachment'; file: File } // Voice | { type: 'startVoiceRecording' } | { type: 'stopVoiceRecording' } | { type: 'voiceTranscriptionCompleted'; transcript: string };
Dependencies
interface StreamingChatDependencies { // Message handler (required) sendMessage: ( content: string, attachments: MessageAttachment[], signal: AbortSignal ) => AsyncGenerator<string, void, unknown>; // File upload (optional) uploadFile?: (file: File) => Promise<MessageAttachment>; // Voice transcription (optional) transcribeVoice?: (audioBlob: Blob) => Promise<string>; }
Complete Example
<script lang="ts"> import { createStore, Effect } from '@composable-svelte/core'; import { FullStreamingChat, streamingChatReducer, createInitialStreamingChatState, type MessageAttachment } from '@composable-svelte/chat'; // Create chat store with all features const chatStore = createStore({ initialState: createInitialStreamingChatState(), reducer: streamingChatReducer, dependencies: { // Stream from OpenAI sendMessage: async function*(content, attachments, signal) { const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${OPENAI_API_KEY}` }, body: JSON.stringify({ model: 'gpt-4', messages: [ { role: 'user', content } ], stream: true }), signal }); const reader = response.body!.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n').filter(line => line.trim().startsWith('data:')); for (const line of lines) { const data = line.replace('data: ', ''); if (data === '[DONE]') break; try { const parsed = JSON.parse(data); const content = parsed.choices[0]?.delta?.content; if (content) { yield content; } } catch (e) { // Skip invalid JSON } } } }, // Upload files to your storage uploadFile: async (file: File) => { const formData = new FormData(); formData.append('file', file); const response = await fetch('/api/upload', { method: 'POST', body: formData }); const { url, id } = await response.json(); return { id, type: file.type.startsWith('image/') ? 'image' : 'file', url, name: file.name, size: file.size }; }, // Transcribe voice using Whisper transcribeVoice: async (audioBlob: Blob) => { const formData = new FormData(); formData.append('file', audioBlob, 'recording.webm'); formData.append('model', 'whisper-1'); const response = await fetch('https://api.openai.com/v1/audio/transcriptions', { method: 'POST', headers: { 'Authorization': `Bearer ${OPENAI_API_KEY}` }, body: formData }); const { text } = await response.json(); return text; } } }); </script> <div class="chat-container"> <FullStreamingChat {chatStore} showReactions={true} showAttachments={true} showVoiceInput={true} /> <!-- Error display --> {#if $chatStore.error} <div class="error-toast">{$chatStore.error}</div> {/if} </div>
COLLABORATIVE FEATURES
Purpose: Real-time presence tracking, typing indicators, and live cursors for multi-user chat.
Quick Start
import { createStore } from '@composable-svelte/core'; import { StandardStreamingChat, collaborativeReducer, createInitialCollaborativeState, PresenceAvatarStack, TypingIndicator } from '@composable-svelte/chat'; // Create collaborative chat store const chatStore = createStore({ initialState: createInitialCollaborativeState(), reducer: collaborativeReducer, dependencies: { sendMessage: /* ... */, connectWebSocket: (conversationId, userId, onMessage, onConnectionChange) => { const ws = new WebSocket(`wss://api.example.com/chat/${conversationId}`); ws.onopen = () => { onConnectionChange({ status: 'connected', connectedAt: Date.now() }); ws.send(JSON.stringify({ type: 'join', userId })); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); onMessage(message); }; ws.onclose = () => { onConnectionChange({ status: 'disconnected' }); }; return () => ws.close(); }, sendWebSocketMessage: async (message) => { ws.send(JSON.stringify(message)); } } }); // Connect to conversation chatStore.dispatch({ type: 'connectToConversation', conversationId: 'chat-123', userId: 'user-456' });
Collaborative State
interface CollaborativeStreamingChatState extends StreamingChatState { // Connection connection: WebSocketConnectionState; conversationId: string | null; currentUserId: string | null; // Users users: Map<string, CollaborativeUser>; // Sync pendingActions: PendingAction[]; syncState: SyncState; } interface CollaborativeUser { id: string; name: string; avatar?: string; color: string; presence: 'active' | 'idle' | 'away' | 'offline'; typing: TypingInfo | null; cursor: CursorPosition | null; permissions: UserPermissions; lastSeen: number; lastHeartbeat: number; }
Presence Components
PresenceBadge:
<PresenceBadge presence="active" showText={true} />
PresenceAvatarStack:
import { PresenceAvatarStack, getActiveUsers } from '@composable-svelte/chat'; const activeUsers = $derived(getActiveUsers($chatStore.users, currentUserId)); <PresenceAvatarStack users={activeUsers} maxVisible={5} />
PresenceList:
<PresenceList users={activeUsers} groupByPresence={true} />
Typing Indicators
TypingIndicator:
import { TypingIndicator, getTypingUsers } from '@composable-svelte/chat'; const typingUsers = $derived( getTypingUsers($chatStore.users, currentUserId, 'message') ); <TypingIndicator users={typingUsers} />
TypingUsersList:
<TypingUsersList users={typingUsers} />
Cursor Tracking
CursorMarker:
<CursorMarker user={user} position={user.cursor} />
CursorOverlay:
import { CursorOverlay, getCursorPositions } from '@composable-svelte/chat'; const cursorPositions = $derived( getCursorPositions($chatStore.users, currentUserId) ); <CursorOverlay cursors={cursorPositions} />
Collaborative Actions
type CollaborativeAction = // Connection | { type: 'connectToConversation'; conversationId: string; userId: string } | { type: 'disconnectFromConversation' } | { type: 'connectionStateChanged'; state: WebSocketConnectionState } // Users | { type: 'userJoined'; user: CollaborativeUser } | { type: 'userLeft'; userId: string } | { type: 'userPresenceChanged'; userId: string; presence: UserPresence } // Typing | { type: 'userStartedTyping'; userId: string; info: TypingInfo } | { type: 'userStoppedTyping'; userId: string } // Cursors | { type: 'userCursorMoved'; userId: string; position: CursorPosition } // Sync | { type: 'syncMessage'; messageId: string; action: string } | { type: 'syncCompleted'; messageId: string };
Complete Collaborative Example
<script lang="ts"> import { createStore } from '@composable-svelte/core'; import { StandardStreamingChat, collaborativeReducer, createInitialCollaborativeState, PresenceAvatarStack, TypingIndicator, getActiveUsers, getTypingUsers, type CollaborativeUser } from '@composable-svelte/chat'; const currentUserId = 'user-123'; // Create WebSocket connection let ws: WebSocket; const chatStore = createStore({ initialState: createInitialCollaborativeState(), reducer: collaborativeReducer, dependencies: { sendMessage: /* ... */, connectWebSocket: (conversationId, userId, onMessage, onConnectionChange) => { ws = new WebSocket(`wss://api.example.com/chat/${conversationId}`); ws.onopen = () => { onConnectionChange({ status: 'connected', connectedAt: Date.now() }); ws.send(JSON.stringify({ type: 'join', userId, user: { id: userId, name: 'Current User', color: '#3b82f6' } })); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); onMessage(message); }; ws.onerror = () => { onConnectionChange({ status: 'error', error: 'Connection failed' }); }; ws.onclose = () => { onConnectionChange({ status: 'disconnected' }); }; return () => { ws.close(); }; }, sendWebSocketMessage: async (message) => { if (ws?.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(message)); } } } }); // Connect on mount $effect(() => { chatStore.dispatch({ type: 'connectToConversation', conversationId: 'chat-room-123', userId: currentUserId }); return () => { chatStore.dispatch({ type: 'disconnectFromConversation' }); }; }); // Emit typing events let typingTimeout: ReturnType<typeof setTimeout>; function handleInputChange(value: string) { chatStore.dispatch({ type: 'inputChanged', value }); // Emit typing started chatStore.dispatch({ type: 'userStartedTyping', userId: currentUserId, info: { target: 'message', startedAt: Date.now(), lastUpdate: Date.now() } }); // Clear previous timeout clearTimeout(typingTimeout); // Stop typing after 2 seconds of inactivity typingTimeout = setTimeout(() => { chatStore.dispatch({ type: 'userStoppedTyping', userId: currentUserId }); }, 2000); } // Derived state const activeUsers = $derived(getActiveUsers($chatStore.users, currentUserId)); const typingUsers = $derived(getTypingUsers($chatStore.users, currentUserId, 'message')); </script> <div class="collaborative-chat"> <!-- Presence indicators --> <div class="chat-header"> <h2>Team Chat</h2> <PresenceAvatarStack users={activeUsers} maxVisible={5} /> </div> <!-- Chat interface --> <StandardStreamingChat {chatStore} /> <!-- Typing indicator --> {#if typingUsers.length > 0} <TypingIndicator users={typingUsers} /> {/if} <!-- Connection status --> <div class="connection-status"> {$chatStore.connection.status} {#if $chatStore.connection.status === 'connected'} <span class="indicator active"></span> {:else} <span class="indicator inactive"></span> {/if} </div> </div>
WEBSOCKET MANAGER
Purpose: Low-level WebSocket management for custom implementations.
API
import { WebSocketManager, createWebSocketManager, type WebSocketConfig } from '@composable-svelte/chat'; const config: WebSocketConfig = { url: 'wss://api.example.com/chat', reconnect: true, reconnectInterval: 1000, maxReconnectAttempts: 10, heartbeatInterval: 30000 }; const manager = createWebSocketManager(config); // Connect manager.connect(); // Send message manager.send({ type: 'message', content: 'Hello' }); // Listen for messages manager.onMessage((message) => { console.log('Received:', message); }); // Listen for connection changes manager.onConnectionChange((state) => { console.log('Connection:', state.status); }); // Disconnect manager.disconnect();
MOCK UTILITIES
Purpose: Testing and development without backend.
import { createMockStreamingChat } from '@composable-svelte/chat'; const chatStore = createStore({ initialState: createInitialStreamingChatState(), reducer: streamingChatReducer, dependencies: createMockStreamingChat({ responseDelay: 50, // Delay between chunks (ms) mockResponses: [ 'This is a mock response.', 'It simulates streaming behavior.' ] }) });
COMPONENT SELECTION GUIDE
When to use each variant:
MinimalStreamingChat:
- Embedded chat
- Minimal UI needed
- Custom chrome/header
StandardStreamingChat (recommended):
- Most use cases
- Balanced features
- Good defaults
FullStreamingChat:
- Feature-rich chat app
- Need reactions
- Need attachments
- Need voice input
Collaborative Features:
- Multi-user chat
- Team collaboration
- Need presence/typing
- Real-time sync required
CROSS-REFERENCES
Related Skills:
- composable-svelte-core: Store, reducer, Effect system
- composable-svelte-media: VoiceInput (can integrate with chat)
- composable-svelte-code: Syntax highlighting in messages
- composable-svelte-components: UI components
When to Use Each Package:
- chat: Real-time chat, streaming responses, LLM interfaces
- media: Audio players, video embeds, standalone voice input
- code: Code editors, syntax highlighting
- core: Base architecture (Store, reducer, effects)
TESTING PATTERNS
StreamingChat Testing
import { TestStore } from '@composable-svelte/core'; import { streamingChatReducer, createInitialStreamingChatState, createMockStreamingChat } from '@composable-svelte/chat'; const store = new TestStore({ initialState: createInitialStreamingChatState(), reducer: streamingChatReducer, dependencies: createMockStreamingChat() }); // Test message send await store.send({ type: 'sendMessage', content: 'Hello', attachments: [] }); await store.receive({ type: 'streamingStarted' }, (state) => { expect(state.isStreaming).toBe(true); }); await store.receive({ type: 'streamingChunk', chunk: 'Hi' }, (state) => { expect(state.streamingMessage).toBe('Hi'); }); await store.receive({ type: 'streamingCompleted' }, (state) => { expect(state.isStreaming).toBe(false); expect(state.messages.length).toBe(2); // User + assistant });
Collaborative Testing
import { TestStore } from '@composable-svelte/core'; import { collaborativeReducer, createInitialCollaborativeState } from '@composable-svelte/chat'; const store = new TestStore({ initialState: createInitialCollaborativeState(), reducer: collaborativeReducer, dependencies: { sendMessage: async function*() { yield 'Test response'; }, connectWebSocket: vi.fn(), sendWebSocketMessage: vi.fn() } }); // Test user join await store.send({ type: 'userJoined', user: { id: 'user-2', name: 'Alice', color: '#3b82f6', presence: 'active' } }, (state) => { expect(state.users.size).toBe(1); expect(state.users.get('user-2')?.name).toBe('Alice'); }); // Test typing indicator await store.send({ type: 'userStartedTyping', userId: 'user-2', info: { target: 'message', startedAt: Date.now(), lastUpdate: Date.now() } }, (state) => { expect(state.users.get('user-2')?.typing).toBeTruthy(); });
TROUBLESHOOTING
Streaming not working:
- Check sendMessage generator function returns AsyncGenerator
- Verify yield statements send string chunks
- Ensure signal.aborted is checked for cancellation
- Check network tab for response streaming
WebSocket connection failing:
- Verify WebSocket URL (wss:// for HTTPS, ws:// for HTTP)
- Check CORS/authentication headers
- Ensure reconnect logic is enabled
- Check browser console for errors
Markdown not rendering:
- Verify message content is valid markdown
- Check syntax highlighting CSS is loaded
- Ensure code blocks use supported languages
Collaborative features not syncing:
- Verify WebSocket connection is active
- Check message format matches expected schema
- Ensure user IDs are unique and consistent
- Check heartbeat/presence update intervals