$npx -y skills add mjunaidca/mjs-agent-skills --skill chatkit-streamingImplements real-time streaming UI patterns for ChatKit applications. This skill should be used when adding response lifecycle management, progress indicators, client effects, and thread state synchronization. Covers onResponseStart/End, onEffect, ProgressUpdateEvent, and thread l
| 1 | # ChatKit Streaming Skill |
| 2 | |
| 3 | ## Overview |
| 4 | |
| 5 | This skill provides patterns for building responsive, real-time ChatKit interfaces. It covers the streaming layer between basic integration and full interactive widgets - making the UI feel alive during AI responses. |
| 6 | |
| 7 | ## Core Concepts |
| 8 | |
| 9 | ### Response Lifecycle |
| 10 | |
| 11 | ChatKit streams responses in real-time. The lifecycle: |
| 12 | |
| 13 | ``` |
| 14 | User sends message |
| 15 | ↓ |
| 16 | onResponseStart() fires |
| 17 | ↓ |
| 18 | [Streaming: tokens arrive, ProgressUpdateEvents shown] |
| 19 | ↓ |
| 20 | onResponseEnd() fires |
| 21 | ↓ |
| 22 | UI unlocks, ready for next interaction |
| 23 | ``` |
| 24 | |
| 25 | ### Client Effects vs Client Tools |
| 26 | |
| 27 | | Type | Direction | Response Required | Use Case | |
| 28 | |------|-----------|-------------------|----------| |
| 29 | | **Client Effect** | Server → Client | No (fire-and-forget) | Update UI state, show notifications | |
| 30 | | **Client Tool** | Server → Client → Server | Yes (return value) | Get client state for AI decision | |
| 31 | |
| 32 | ## Implementation Patterns |
| 33 | |
| 34 | ### Pattern 1: Response Lifecycle Handlers |
| 35 | |
| 36 | **When**: Lock UI during AI response, show loading states, prevent race conditions |
| 37 | |
| 38 | **Frontend Implementation**: |
| 39 | ```typescript |
| 40 | import { useChatKit } from "@openai/chatkit-react"; |
| 41 | |
| 42 | function ChatWithLifecycle() { |
| 43 | const [isResponding, setIsResponding] = useState(false); |
| 44 | const lockInteraction = useAppStore((s) => s.lockInteraction); |
| 45 | const unlockInteraction = useAppStore((s) => s.unlockInteraction); |
| 46 | |
| 47 | const chatkit = useChatKit({ |
| 48 | api: { url: API_URL, domainKey: DOMAIN_KEY }, |
| 49 | |
| 50 | onResponseStart: () => { |
| 51 | setIsResponding(true); |
| 52 | lockInteraction(); // Disable map/canvas/form interactions |
| 53 | }, |
| 54 | |
| 55 | onResponseEnd: () => { |
| 56 | setIsResponding(false); |
| 57 | unlockInteraction(); |
| 58 | }, |
| 59 | |
| 60 | onReady: () => { |
| 61 | console.log("ChatKit initialized"); |
| 62 | }, |
| 63 | |
| 64 | onError: ({ error }) => { |
| 65 | console.error("ChatKit error:", error); |
| 66 | setIsResponding(false); |
| 67 | unlockInteraction(); |
| 68 | }, |
| 69 | }); |
| 70 | |
| 71 | return ( |
| 72 | <div> |
| 73 | {isResponding && <LoadingOverlay />} |
| 74 | <ChatKit control={chatkit.control} /> |
| 75 | </div> |
| 76 | ); |
| 77 | } |
| 78 | ``` |
| 79 | |
| 80 | **Evidence**: `metro-map/frontend/src/components/ChatKitPanel.tsx:205-210` |
| 81 | |
| 82 | ### Pattern 2: Client Effects (Fire-and-Forget) |
| 83 | |
| 84 | **When**: Server needs to update client UI without expecting a response |
| 85 | |
| 86 | **Backend - Streaming Effects**: |
| 87 | ```python |
| 88 | from chatkit.types import ClientEffectEvent |
| 89 | |
| 90 | async def respond(self, thread, item, context): |
| 91 | # ... agent processing ... |
| 92 | |
| 93 | # Fire client effect to update UI |
| 94 | yield ClientEffectEvent( |
| 95 | name="update_cat_status", |
| 96 | data={ |
| 97 | "state": {"energy": 80, "happiness": 90}, |
| 98 | "flash": "Cat is now happy!" |
| 99 | } |
| 100 | ) |
| 101 | |
| 102 | # Another effect - speech bubble |
| 103 | yield ClientEffectEvent( |
| 104 | name="cat_say", |
| 105 | data={"message": "Meow!"} |
| 106 | ) |
| 107 | ``` |
| 108 | |
| 109 | **Frontend - Handling Effects**: |
| 110 | ```typescript |
| 111 | const chatkit = useChatKit({ |
| 112 | api: { url: API_URL, domainKey: DOMAIN_KEY }, |
| 113 | |
| 114 | onEffect: ({ name, data }) => { |
| 115 | switch (name) { |
| 116 | case "update_cat_status": |
| 117 | const catState = data.state as CatStatePayload; |
| 118 | applyCatUpdate(catState); |
| 119 | if (data.flash) { |
| 120 | setFlashMessage(data.flash as string); |
| 121 | } |
| 122 | break; |
| 123 | |
| 124 | case "cat_say": |
| 125 | setSpeech({ message: String(data.message) }); |
| 126 | break; |
| 127 | |
| 128 | case "location_select_mode": |
| 129 | setLocationSelectLineId(data.lineId as string); |
| 130 | break; |
| 131 | |
| 132 | case "add_station": |
| 133 | clearLocationSelectMode(); |
| 134 | if (data.map) setMap(data.map as MetroMap); |
| 135 | if (data.stationId) { |
| 136 | requestAnimationFrame(() => focusStation(data.stationId)); |
| 137 | } |
| 138 | break; |
| 139 | } |
| 140 | }, |
| 141 | }); |
| 142 | ``` |
| 143 | |
| 144 | **Evidence**: |
| 145 | - `cat-lounge/backend/app/cat_agent.py` (server-side effects) |
| 146 | - `cat-lounge/frontend/src/components/ChatKitPanel.tsx:86-103` (frontend handler) |
| 147 | - `metro-map/frontend/src/components/ChatKitPanel.tsx:130-153` |
| 148 | |
| 149 | ### Pattern 3: Progress Updates |
| 150 | |
| 151 | **When**: Show "Searching...", "Loading...", "Analyzing..." during long operations |
| 152 | |
| 153 | **Backend - Streaming Progress**: |
| 154 | ```python |
| 155 | from chatkit.types import ProgressUpdateEvent |
| 156 | |
| 157 | @function_tool |
| 158 | async def search_articles(ctx: AgentContext, query: str) -> str: |
| 159 | """Search for articles matching the query.""" |
| 160 | |
| 161 | # Show progress to user |
| 162 | yield ProgressUpdateEvent(message="Searching articles...") |
| 163 | |
| 164 | results = await article_store.search(query) |
| 165 | |
| 166 | yield ProgressUpdateEvent(message=f"Found {len(results)} articles...") |
| 167 | |
| 168 | # Process results |
| 169 | for i, article in enumerate(results): |
| 170 | if i % 5 == 0: |
| 171 | yield ProgressUpdateEvent( |
| 172 | message=f"Processing article {i+1}/{len(results)}..." |
| 173 | ) |
| 174 | # ... process article |
| 175 | |
| 176 | return format_results(r |