--- name: molykit description: | CRITICAL: Use for MolyKit AI chat toolkit. Triggers on: BotClient, OpenAI, SSE streaming, AI chat, molykit, PlatformSend, spawn(), ThreadToken, cross-platform async, Chat widget, Messages, PromptInput, Avatar, LLM --- # MolyKit Skill Best practices for building AI chat interfaces with Makepad using MolyKit - a toolkit for cross-platform AI chat applications. **Source codebase**: `/Users/zhangalex/Work/Projects/FW/robius/moly/moly-kit` ## Triggers Use this skill when: - Building AI chat interfaces with Makepad - Integrating OpenAI or other LLM APIs - Implementing cross-platform async for native and WASM - Creating chat widgets (messages, prompts, avatars) - Handling SSE streaming responses - Keywords: molykit, moly-kit, ai chat, bot client, openai makepad, chat widget, sse streaming ## Overview MolyKit provides: - Cross-platform async utilities (PlatformSend, spawn(), ThreadToken) - Ready-to-use chat widgets (Chat, Messages, PromptInput, Avatar) - BotClient trait for AI provider integration - OpenAI-compatible client with SSE streaming - Protocol types for messages, bots, and tool calls - MCP (Model Context Protocol) support ## Cross-Platform Async Patterns ### PlatformSend - Send Only on Native ```rust /// Implies Send only on native platforms, not on WASM /// - On native: implemented by types that implement Send /// - On WASM: implemented by ALL types pub trait PlatformSend: PlatformSendInner {} /// Boxed future type for cross-platform use pub type BoxPlatformSendFuture<'a, T> = Pin + 'a>>; /// Boxed stream type for cross-platform use pub type BoxPlatformSendStream<'a, T> = Pin + 'a>>; ``` ### Platform-Agnostic Spawning ```rust /// Runs a future independently /// - Uses tokio on native (requires Send) /// - Uses wasm-bindgen-futures on WASM (no Send required) pub fn spawn(fut: impl PlatformSendFuture + 'static); // Usage spawn(async move { let result = fetch_data().await; Cx::post_action(DataReady(result)); SignalToUI::set_ui_signal(); }); ``` ### Task Cancellation with AbortOnDropHandle ```rust /// Handle that aborts its future when dropped pub struct AbortOnDropHandle(AbortHandle); // Usage - task cancelled when widget dropped #[rust] task_handle: Option, fn start_task(&mut self) { let (future, handle) = abort_on_drop(async move { // async work... }); self.task_handle = Some(handle); spawn(async move { let _ = future.await; }); } ``` ### ThreadToken for Non-Send Types on WASM ```rust /// Store non-Send value in thread-local, access via token pub struct ThreadToken; impl ThreadToken { pub fn new(value: T) -> Self; pub fn peek(&self, f: impl FnOnce(&T) -> R) -> R; pub fn peek_mut(&self, f: impl FnOnce(&mut T) -> R) -> R; } // Usage - wrap non-Send type for use across Send boundaries let token = ThreadToken::new(non_send_value); spawn(async move { token.peek(|value| { // use value... }); }); ``` ## BotClient Trait ### Implementing AI Provider Integration ```rust pub trait BotClient: Send { /// Send message with streamed response fn send( &mut self, bot_id: &BotId, messages: &[Message], tools: &[Tool], ) -> BoxPlatformSendStream<'static, ClientResult>; /// Get available bots/models fn bots(&self) -> BoxPlatformSendFuture<'static, ClientResult>>; /// Clone for passing around fn clone_box(&self) -> Box; } // Usage let client = OpenAIClient::new("https://api.openai.com/v1".into()); client.set_key("sk-...")?; let context = BotContext::from(client); ``` ### BotContext - Sharable Wrapper ```rust /// Sharable wrapper with loaded bots for sync UI access pub struct BotContext(Arc>); impl BotContext { pub fn load(&mut self) -> BoxPlatformSendFuture>; pub fn bots(&self) -> Vec; pub fn get_bot(&self, id: &BotId) -> Option; pub fn client(&self) -> Box; } // Usage let mut context = BotContext::from(client); spawn(async move { if let Err(errors) = context.load().await.into_result() { // handle errors } Cx::post_action(BotsLoaded); }); ``` ## Protocol Types ### Message Structure ```rust pub struct Message { pub from: EntityId, // User, System, Bot(BotId), App pub metadata: MessageMetadata, pub content: MessageContent, } pub struct MessageContent { pub text: String, // Main content (markdown) pub reasoning: String, // AI reasoning/thinking pub citations: Vec, // Source URLs pub attachments: Vec, pub tool_calls: Vec, pub tool_results: Vec, } pub struct MessageMetadata { pub is_writing: bool, // Still being streamed pub created_at: DateTime, } ``` ### Bot Identification ```rust /// Globally unique bot ID: ;@ pub struct BotId(Arc); impl BotId { pub fn new(id: &str, provider: &str) -> Self; pub fn id(&self) -> &str; // provider-local id pub fn provider(&self) -> &str; // provider domain } // Example: BotId::new("gpt-4", "api.openai.com") // -> "5;gpt-4@api.openai.com" ``` ## Widget Patterns ### Slot Widget - Runtime Content Replacement ```rust live_design! { pub Slot = {{Slot}} { width: Fill, height: Fit, slot = {} // default content } } // Usage - replace content at runtime let mut slot = widget.slot(id!(content)); if let Some(custom) = client.content_widget(cx, ...) { slot.replace(custom); } else { slot.restore(); // back to default slot.default().as_standard_message_content().set_content(cx, &content); } ``` ### Avatar Widget - Text/Image Toggle ```rust live_design! { pub Avatar = {{Avatar}} { grapheme = { visible: false, label =