[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71990DFH0VASR9ASJSERQCD":3,"skill-same:01M71990DFH0VASR9ASJSERQCD":64},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":38,"linkOnly":39,"markdownContent":40,"markdownBody":41,"frontMatter":42,"refName":44,"commitSha":45,"skillDirectory":46,"forksCount":14,"repositoryHtmlUrl":47,"repositoryHomepageUrl":6,"ownerHtmlUrl":48,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":49,"install":50,"resources":63},"01M71990DFH0VASR9ASJSERQCD","molykit",null,"CRITICAL: Use for MolyKit AI chat toolkit. Triggers on:\nBotClient, OpenAI, SSE streaming, AI chat, molykit,\nPlatformSend, spawn(), ThreadToken, cross-platform async,\nChat widget, Messages, PromptInput, Avatar, LLM","sapsapshen\u002Fai-memory-router","sapsapshen","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F20347959?v=4","01M6XDQEF1DP50DKF37H81J3J5","shenmian","01M6XDQEJPE41Y3DT2763X8ZJN",0,"MIT","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fglobal\u002Fmolykit\u002FSKILL.md",[],[5,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"chat widget","unknown","for","avatar","critical","triggers","platform","widget","chat","toolkit","community","async","sse streaming","use","spawn","sse","messages","streaming","openai","2026-11-10 10:12:30",false,"---\nname: molykit\ndescription: |\n  CRITICAL: Use for MolyKit AI chat toolkit. Triggers on:\n  BotClient, OpenAI, SSE streaming, AI chat, molykit,\n  PlatformSend, spawn(), ThreadToken, cross-platform async,\n  Chat widget, Messages, PromptInput, Avatar, LLM\nrisk: unknown\nsource: community\n---\n\n# MolyKit Skill\n\nBest practices for building AI chat interfaces with Makepad using MolyKit - a toolkit for cross-platform AI chat applications.\n\n**Source codebase**: `\u002FUsers\u002Fzhangalex\u002FWork\u002FProjects\u002FFW\u002Frobius\u002Fmoly\u002Fmoly-kit`\n\n## When to Use\n\nUse this skill when:\n- Building AI chat interfaces with Makepad\n- Integrating OpenAI or other LLM APIs\n- Implementing cross-platform async for native and WASM\n- Creating chat widgets (messages, prompts, avatars)\n- Handling SSE streaming responses\n- Keywords: molykit, moly-kit, ai chat, bot client, openai makepad, chat widget, sse streaming\n\n## Overview\n\nMolyKit provides:\n- Cross-platform async utilities (PlatformSend, spawn(), ThreadToken)\n- Ready-to-use chat widgets (Chat, Messages, PromptInput, Avatar)\n- BotClient trait for AI provider integration\n- OpenAI-compatible client with SSE streaming\n- Protocol types for messages, bots, and tool calls\n- MCP (Model Context Protocol) support\n\n## Cross-Platform Async Patterns\n\n### PlatformSend - Send Only on Native\n\n```rust\n\u002F\u002F\u002F Implies Send only on native platforms, not on WASM\n\u002F\u002F\u002F - On native: implemented by types that implement Send\n\u002F\u002F\u002F - On WASM: implemented by ALL types\npub trait PlatformSend: PlatformSendInner {}\n\n\u002F\u002F\u002F Boxed future type for cross-platform use\npub type BoxPlatformSendFuture\u003C'a, T> = Pin\u003CBox\u003Cdyn PlatformSendFuture\u003COutput = T> + 'a>>;\n\n\u002F\u002F\u002F Boxed stream type for cross-platform use\npub type BoxPlatformSendStream\u003C'a, T> = Pin\u003CBox\u003Cdyn PlatformSendStream\u003CItem = T> + 'a>>;\n```\n\n### Platform-Agnostic Spawning\n\n```rust\n\u002F\u002F\u002F Runs a future independently\n\u002F\u002F\u002F - Uses tokio on native (requires Send)\n\u002F\u002F\u002F - Uses wasm-bindgen-futures on WASM (no Send required)\npub fn spawn(fut: impl PlatformSendFuture\u003COutput = ()> + 'static);\n\n\u002F\u002F Usage\nspawn(async move {\n    let result = fetch_data().await;\n    Cx::post_action(DataReady(result));\n    SignalToUI::set_ui_signal();\n});\n```\n\n### Task Cancellation with AbortOnDropHandle\n\n```rust\n\u002F\u002F\u002F Handle that aborts its future when dropped\npub struct AbortOnDropHandle(AbortHandle);\n\n\u002F\u002F Usage - task cancelled when widget dropped\n#[rust]\ntask_handle: Option\u003CAbortOnDropHandle>,\n\nfn start_task(&mut self) {\n    let (future, handle) = abort_on_drop(async move {\n        \u002F\u002F async work...\n    });\n    self.task_handle = Some(handle);\n    spawn(async move { let _ = future.await; });\n}\n```\n\n### ThreadToken for Non-Send Types on WASM\n\n```rust\n\u002F\u002F\u002F Store non-Send value in thread-local, access via token\npub struct ThreadToken\u003CT: 'static>;\n\nimpl\u003CT> ThreadToken\u003CT> {\n    pub fn new(value: T) -> Self;\n    pub fn peek\u003CR>(&self, f: impl FnOnce(&T) -> R) -> R;\n    pub fn peek_mut\u003CR>(&self, f: impl FnOnce(&mut T) -> R) -> R;\n}\n\n\u002F\u002F Usage - wrap non-Send type for use across Send boundaries\nlet token = ThreadToken::new(non_send_value);\nspawn(async move {\n    token.peek(|value| {\n        \u002F\u002F use value...\n    });\n});\n```\n\n## BotClient Trait\n\n### Implementing AI Provider Integration\n\n```rust\npub trait BotClient: Send {\n    \u002F\u002F\u002F Send message with streamed response\n    fn send(\n        &mut self,\n        bot_id: &BotId,\n        messages: &[Message],\n        tools: &[Tool],\n    ) -> BoxPlatformSendStream\u003C'static, ClientResult\u003CMessageContent>>;\n\n    \u002F\u002F\u002F Get available bots\u002Fmodels\n    fn bots(&self) -> BoxPlatformSendFuture\u003C'static, ClientResult\u003CVec\u003CBot>>>;\n\n    \u002F\u002F\u002F Clone for passing around\n    fn clone_box(&self) -> Box\u003Cdyn BotClient>;\n}\n\n\u002F\u002F Usage\nlet client = OpenAIClient::new(\"https:\u002F\u002Fapi.openai.com\u002Fv1\".into());\nclient.set_key(\"sk-...\")?;\nlet context = BotContext::from(client);\n```\n\n### BotContext - Sharable Wrapper\n\n```rust\n\u002F\u002F\u002F Sharable wrapper with loaded bots for sync UI access\npub struct BotContext(Arc\u003CMutex\u003CInnerBotContext>>);\n\nimpl BotContext {\n    pub fn load(&mut self) -> BoxPlatformSendFuture\u003CClientResult\u003C()>>;\n    pub fn bots(&self) -> Vec\u003CBot>;\n    pub fn get_bot(&self, id: &BotId) -> Option\u003CBot>;\n    pub fn client(&self) -> Box\u003Cdyn BotClient>;\n}\n\n\u002F\u002F Usage\nlet mut context = BotContext::from(client);\nspawn(async move {\n    if let Err(errors) = context.load().await.into_result() {\n        \u002F\u002F handle errors\n    }\n    Cx::post_action(BotsLoaded);\n});\n```\n\n## Protocol Types\n\n### Message Structure\n\n```rust\npub struct Message {\n    pub from: EntityId,         \u002F\u002F User, System, Bot(BotId), App\n    pub metadata: MessageMetadata,\n    pub content: MessageContent,\n}\n\npub struct MessageContent {\n    pub text: String,           \u002F\u002F Main content (markdown)\n    pub reasoning: String,      \u002F\u002F AI reasoning\u002Fthinking\n    pub citations: Vec\u003CString>, \u002F\u002F Source URLs\n    pub attachments: Vec\u003CAttachment>,\n    pub tool_calls: Vec\u003CToolCall>,\n    pub tool_results: Vec\u003CToolResult>,\n}\n\npub struct MessageMetadata {\n    pub is_writing: bool,       \u002F\u002F Still being streamed\n    pub created_at: DateTime\u003CUtc>,\n}\n```\n\n### Bot Identification\n\n```rust\n\u002F\u002F\u002F Globally unique bot ID: \u003Clen>;\u003Cid>@\u003Cprovider>\npub struct BotId(Arc\u003Cstr>);\n\nimpl BotId {\n    pub fn new(id: &str, provider: &str) -> Self;\n    pub fn id(&self) -> &str;       \u002F\u002F provider-local id\n    pub fn provider(&self) -> &str; \u002F\u002F provider domain\n}\n\n\u002F\u002F Example: BotId::new(\"gpt-4\", \"api.openai.com\")\n\u002F\u002F -> \"5;gpt-4@api.openai.com\"\n```\n\n## Widget Patterns\n\n### Slot Widget - Runtime Content Replacement\n\n```rust\nlive_design! {\n    pub Slot = {{Slot}} {\n        width: Fill, height: Fit,\n        slot = \u003CView> {}  \u002F\u002F default content\n    }\n}\n\n\u002F\u002F Usage - replace content at runtime\nlet mut slot = widget.slot(id!(content));\nif let Some(custom) = client.content_widget(cx, ...) {\n    slot.replace(custom);\n} else {\n    slot.restore();  \u002F\u002F back to default\n    slot.default().as_standard_message_content().set_content(cx, &content);\n}\n```\n\n### Avatar Widget - Text\u002FImage Toggle\n\n```rust\nlive_design! {\n    pub Avatar = {{Avatar}} \u003CView> {\n        grapheme = \u003CRoundedView> {\n            visible: false,\n            label = \u003CLabel> { text: \"P\" }\n        }\n        dependency = \u003CRoundedView> {\n            visible: false,\n            image = \u003CImage> {}\n        }\n    }\n}\n\nimpl Widget for Avatar {\n    fn draw_walk(&mut self, cx: &mut Cx2d, ...) -> DrawStep {\n        if let Some(avatar) = &self.avatar {\n            match avatar {\n                Picture::Grapheme(g) => {\n                    self.view(id!(grapheme)).set_visible(cx, true);\n                    self.view(id!(dependency)).set_visible(cx, false);\n                    self.label(id!(label)).set_text(cx, &g);\n                }\n                Picture::Dependency(d) => {\n                    self.view(id!(dependency)).set_visible(cx, true);\n                    self.view(id!(grapheme)).set_visible(cx, false);\n                    self.image(id!(image)).load_image_dep_by_path(cx, d.as_str());\n                }\n            }\n        }\n        self.deref.draw_walk(cx, scope, walk)\n    }\n}\n```\n\n### PromptInput Widget\n\n```rust\n#[derive(Live, Widget)]\npub struct PromptInput {\n    #[deref] deref: CommandTextInput,\n    #[live] pub send_icon: LiveValue,\n    #[live] pub stop_icon: LiveValue,\n    #[rust] pub task: Task,           \u002F\u002F Send or Stop\n    #[rust] pub interactivity: Interactivity,\n}\n\nimpl PromptInput {\n    pub fn submitted(&self, actions: &Actions) -> bool;\n    pub fn reset(&mut self, cx: &mut Cx);\n    pub fn set_send(&mut self);\n    pub fn set_stop(&mut self);\n    pub fn enable(&mut self);\n    pub fn disable(&mut self);\n}\n```\n\n### Messages Widget - Conversation View\n\n```rust\n#[derive(Live, Widget)]\npub struct Messages {\n    #[deref] deref: View,\n    #[rust] pub messages: Vec\u003CMessage>,\n    #[rust] pub bot_context: Option\u003CBotContext>,\n}\n\nimpl Messages {\n    pub fn set_messages(&mut self, messages: Vec\u003CMessage>, scroll_to_bottom: bool);\n    pub fn scroll_to_bottom(&mut self, cx: &mut Cx, triggered_by_stream: bool);\n    pub fn is_at_bottom(&self) -> bool;\n}\n```\n\n## UiRunner Pattern for Async-to-UI\n\n```rust\nimpl Widget for PromptInput {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {\n        self.deref.handle_event(cx, event, scope);\n        self.ui_runner().handle(cx, event, scope, self);\n\n        if self.button(id!(attach)).clicked(event.actions()) {\n            let ui = self.ui_runner();\n            Attachment::pick_multiple(move |result| match result {\n                Ok(attachments) => {\n                    ui.defer_with_redraw(move |me, cx, _| {\n                        me.attachment_list_ref().write().attachments.extend(attachments);\n                    });\n                }\n                Err(_) => {}\n            });\n        }\n    }\n}\n```\n\n## SSE Streaming\n\n```rust\n\u002F\u002F\u002F Parse SSE byte stream into message stream\npub fn parse_sse\u003CS, B, E>(s: S) -> impl Stream\u003CItem = Result\u003CString, E>>\nwhere\n    S: Stream\u003CItem = Result\u003CB, E>>,\n    B: AsRef\u003C[u8]>,\n{\n    \u002F\u002F Split on \"\\n\\n\", extract \"data:\" content\n    \u002F\u002F Filter comments and [DONE] messages\n}\n\n\u002F\u002F Usage in BotClient::send\nfn send(&mut self, ...) -> BoxPlatformSendStream\u003C...> {\n    let stream = stream! {\n        let response = client.post(url).send().await?;\n        let events = parse_sse(response.bytes_stream());\n\n        for await event in events {\n            let completion: Completion = serde_json::from_str(&event)?;\n            content.text.push_str(&completion.delta.content);\n            yield ClientResult::new_ok(content.clone());\n        }\n    };\n    Box::pin(stream)\n}\n```\n\n## Best Practices\n\n1. **Use PlatformSend for cross-platform**: Same code works on native and WASM\n2. **Use spawn() not tokio::spawn**: Platform-agnostic task spawning\n3. **Use AbortOnDropHandle**: Cancel tasks when widget drops\n4. **Use ThreadToken for non-Send on WASM**: Thread-local storage with token access\n5. **Use Slot for custom content**: Allow BotClient to provide custom widgets\n6. **Use read()\u002Fwrite() pattern**: Safe borrow access via WidgetRef\n7. **Use UiRunner::defer_with_redraw**: Update widget from async context\n8. **Handle ClientResult partial success**: May have value AND errors\n\n## Reference Files\n\n- `llms.txt` - Complete MolyKit API reference\n","# MolyKit Skill\n\nBest practices for building AI chat interfaces with Makepad using MolyKit - a toolkit for cross-platform AI chat applications.\n\n**Source codebase**: `\u002FUsers\u002Fzhangalex\u002FWork\u002FProjects\u002FFW\u002Frobius\u002Fmoly\u002Fmoly-kit`\n\n## When to Use\n\nUse this skill when:\n- Building AI chat interfaces with Makepad\n- Integrating OpenAI or other LLM APIs\n- Implementing cross-platform async for native and WASM\n- Creating chat widgets (messages, prompts, avatars)\n- Handling SSE streaming responses\n- Keywords: molykit, moly-kit, ai chat, bot client, openai makepad, chat widget, sse streaming\n\n## Overview\n\nMolyKit provides:\n- Cross-platform async utilities (PlatformSend, spawn(), ThreadToken)\n- Ready-to-use chat widgets (Chat, Messages, PromptInput, Avatar)\n- BotClient trait for AI provider integration\n- OpenAI-compatible client with SSE streaming\n- Protocol types for messages, bots, and tool calls\n- MCP (Model Context Protocol) support\n\n## Cross-Platform Async Patterns\n\n### PlatformSend - Send Only on Native\n\n```rust\n\u002F\u002F\u002F Implies Send only on native platforms, not on WASM\n\u002F\u002F\u002F - On native: implemented by types that implement Send\n\u002F\u002F\u002F - On WASM: implemented by ALL types\npub trait PlatformSend: PlatformSendInner {}\n\n\u002F\u002F\u002F Boxed future type for cross-platform use\npub type BoxPlatformSendFuture\u003C'a, T> = Pin\u003CBox\u003Cdyn PlatformSendFuture\u003COutput = T> + 'a>>;\n\n\u002F\u002F\u002F Boxed stream type for cross-platform use\npub type BoxPlatformSendStream\u003C'a, T> = Pin\u003CBox\u003Cdyn PlatformSendStream\u003CItem = T> + 'a>>;\n```\n\n### Platform-Agnostic Spawning\n\n```rust\n\u002F\u002F\u002F Runs a future independently\n\u002F\u002F\u002F - Uses tokio on native (requires Send)\n\u002F\u002F\u002F - Uses wasm-bindgen-futures on WASM (no Send required)\npub fn spawn(fut: impl PlatformSendFuture\u003COutput = ()> + 'static);\n\n\u002F\u002F Usage\nspawn(async move {\n    let result = fetch_data().await;\n    Cx::post_action(DataReady(result));\n    SignalToUI::set_ui_signal();\n});\n```\n\n### Task Cancellation with AbortOnDropHandle\n\n```rust\n\u002F\u002F\u002F Handle that aborts its future when dropped\npub struct AbortOnDropHandle(AbortHandle);\n\n\u002F\u002F Usage - task cancelled when widget dropped\n#[rust]\ntask_handle: Option\u003CAbortOnDropHandle>,\n\nfn start_task(&mut self) {\n    let (future, handle) = abort_on_drop(async move {\n        \u002F\u002F async work...\n    });\n    self.task_handle = Some(handle);\n    spawn(async move { let _ = future.await; });\n}\n```\n\n### ThreadToken for Non-Send Types on WASM\n\n```rust\n\u002F\u002F\u002F Store non-Send value in thread-local, access via token\npub struct ThreadToken\u003CT: 'static>;\n\nimpl\u003CT> ThreadToken\u003CT> {\n    pub fn new(value: T) -> Self;\n    pub fn peek\u003CR>(&self, f: impl FnOnce(&T) -> R) -> R;\n    pub fn peek_mut\u003CR>(&self, f: impl FnOnce(&mut T) -> R) -> R;\n}\n\n\u002F\u002F Usage - wrap non-Send type for use across Send boundaries\nlet token = ThreadToken::new(non_send_value);\nspawn(async move {\n    token.peek(|value| {\n        \u002F\u002F use value...\n    });\n});\n```\n\n## BotClient Trait\n\n### Implementing AI Provider Integration\n\n```rust\npub trait BotClient: Send {\n    \u002F\u002F\u002F Send message with streamed response\n    fn send(\n        &mut self,\n        bot_id: &BotId,\n        messages: &[Message],\n        tools: &[Tool],\n    ) -> BoxPlatformSendStream\u003C'static, ClientResult\u003CMessageContent>>;\n\n    \u002F\u002F\u002F Get available bots\u002Fmodels\n    fn bots(&self) -> BoxPlatformSendFuture\u003C'static, ClientResult\u003CVec\u003CBot>>>;\n\n    \u002F\u002F\u002F Clone for passing around\n    fn clone_box(&self) -> Box\u003Cdyn BotClient>;\n}\n\n\u002F\u002F Usage\nlet client = OpenAIClient::new(\"https:\u002F\u002Fapi.openai.com\u002Fv1\".into());\nclient.set_key(\"sk-...\")?;\nlet context = BotContext::from(client);\n```\n\n### BotContext - Sharable Wrapper\n\n```rust\n\u002F\u002F\u002F Sharable wrapper with loaded bots for sync UI access\npub struct BotContext(Arc\u003CMutex\u003CInnerBotContext>>);\n\nimpl BotContext {\n    pub fn load(&mut self) -> BoxPlatformSendFuture\u003CClientResult\u003C()>>;\n    pub fn bots(&self) -> Vec\u003CBot>;\n    pub fn get_bot(&self, id: &BotId) -> Option\u003CBot>;\n    pub fn client(&self) -> Box\u003Cdyn BotClient>;\n}\n\n\u002F\u002F Usage\nlet mut context = BotContext::from(client);\nspawn(async move {\n    if let Err(errors) = context.load().await.into_result() {\n        \u002F\u002F handle errors\n    }\n    Cx::post_action(BotsLoaded);\n});\n```\n\n## Protocol Types\n\n### Message Structure\n\n```rust\npub struct Message {\n    pub from: EntityId,         \u002F\u002F User, System, Bot(BotId), App\n    pub metadata: MessageMetadata,\n    pub content: MessageContent,\n}\n\npub struct MessageContent {\n    pub text: String,           \u002F\u002F Main content (markdown)\n    pub reasoning: String,      \u002F\u002F AI reasoning\u002Fthinking\n    pub citations: Vec\u003CString>, \u002F\u002F Source URLs\n    pub attachments: Vec\u003CAttachment>,\n    pub tool_calls: Vec\u003CToolCall>,\n    pub tool_results: Vec\u003CToolResult>,\n}\n\npub struct MessageMetadata {\n    pub is_writing: bool,       \u002F\u002F Still being streamed\n    pub created_at: DateTime\u003CUtc>,\n}\n```\n\n### Bot Identification\n\n```rust\n\u002F\u002F\u002F Globally unique bot ID: \u003Clen>;\u003Cid>@\u003Cprovider>\npub struct BotId(Arc\u003Cstr>);\n\nimpl BotId {\n    pub fn new(id: &str, provider: &str) -> Self;\n    pub fn id(&self) -> &str;       \u002F\u002F provider-local id\n    pub fn provider(&self) -> &str; \u002F\u002F provider domain\n}\n\n\u002F\u002F Example: BotId::new(\"gpt-4\", \"api.openai.com\")\n\u002F\u002F -> \"5;gpt-4@api.openai.com\"\n```\n\n## Widget Patterns\n\n### Slot Widget - Runtime Content Replacement\n\n```rust\nlive_design! {\n    pub Slot = {{Slot}} {\n        width: Fill, height: Fit,\n        slot = \u003CView> {}  \u002F\u002F default content\n    }\n}\n\n\u002F\u002F Usage - replace content at runtime\nlet mut slot = widget.slot(id!(content));\nif let Some(custom) = client.content_widget(cx, ...) {\n    slot.replace(custom);\n} else {\n    slot.restore();  \u002F\u002F back to default\n    slot.default().as_standard_message_content().set_content(cx, &content);\n}\n```\n\n### Avatar Widget - Text\u002FImage Toggle\n\n```rust\nlive_design! {\n    pub Avatar = {{Avatar}} \u003CView> {\n        grapheme = \u003CRoundedView> {\n            visible: false,\n            label = \u003CLabel> { text: \"P\" }\n        }\n        dependency = \u003CRoundedView> {\n            visible: false,\n            image = \u003CImage> {}\n        }\n    }\n}\n\nimpl Widget for Avatar {\n    fn draw_walk(&mut self, cx: &mut Cx2d, ...) -> DrawStep {\n        if let Some(avatar) = &self.avatar {\n            match avatar {\n                Picture::Grapheme(g) => {\n                    self.view(id!(grapheme)).set_visible(cx, true);\n                    self.view(id!(dependency)).set_visible(cx, false);\n                    self.label(id!(label)).set_text(cx, &g);\n                }\n                Picture::Dependency(d) => {\n                    self.view(id!(dependency)).set_visible(cx, true);\n                    self.view(id!(grapheme)).set_visible(cx, false);\n                    self.image(id!(image)).load_image_dep_by_path(cx, d.as_str());\n                }\n            }\n        }\n        self.deref.draw_walk(cx, scope, walk)\n    }\n}\n```\n\n### PromptInput Widget\n\n```rust\n#[derive(Live, Widget)]\npub struct PromptInput {\n    #[deref] deref: CommandTextInput,\n    #[live] pub send_icon: LiveValue,\n    #[live] pub stop_icon: LiveValue,\n    #[rust] pub task: Task,           \u002F\u002F Send or Stop\n    #[rust] pub interactivity: Interactivity,\n}\n\nimpl PromptInput {\n    pub fn submitted(&self, actions: &Actions) -> bool;\n    pub fn reset(&mut self, cx: &mut Cx);\n    pub fn set_send(&mut self);\n    pub fn set_stop(&mut self);\n    pub fn enable(&mut self);\n    pub fn disable(&mut self);\n}\n```\n\n### Messages Widget - Conversation View\n\n```rust\n#[derive(Live, Widget)]\npub struct Messages {\n    #[deref] deref: View,\n    #[rust] pub messages: Vec\u003CMessage>,\n    #[rust] pub bot_context: Option\u003CBotContext>,\n}\n\nimpl Messages {\n    pub fn set_messages(&mut self, messages: Vec\u003CMessage>, scroll_to_bottom: bool);\n    pub fn scroll_to_bottom(&mut self, cx: &mut Cx, triggered_by_stream: bool);\n    pub fn is_at_bottom(&self) -> bool;\n}\n```\n\n## UiRunner Pattern for Async-to-UI\n\n```rust\nimpl Widget for PromptInput {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {\n        self.deref.handle_event(cx, event, scope);\n        self.ui_runner().handle(cx, event, scope, self);\n\n        if self.button(id!(attach)).clicked(event.actions()) {\n            let ui = self.ui_runner();\n            Attachment::pick_multiple(move |result| match result {\n                Ok(attachments) => {\n                    ui.defer_with_redraw(move |me, cx, _| {\n                        me.attachment_list_ref().write().attachments.extend(attachments);\n                    });\n                }\n                Err(_) => {}\n            });\n        }\n    }\n}\n```\n\n## SSE Streaming\n\n```rust\n\u002F\u002F\u002F Parse SSE byte stream into message stream\npub fn parse_sse\u003CS, B, E>(s: S) -> impl Stream\u003CItem = Result\u003CString, E>>\nwhere\n    S: Stream\u003CItem = Result\u003CB, E>>,\n    B: AsRef\u003C[u8]>,\n{\n    \u002F\u002F Split on \"\\n\\n\", extract \"data:\" content\n    \u002F\u002F Filter comments and [DONE] messages\n}\n\n\u002F\u002F Usage in BotClient::send\nfn send(&mut self, ...) -> BoxPlatformSendStream\u003C...> {\n    let stream = stream! {\n        let response = client.post(url).send().await?;\n        let events = parse_sse(response.bytes_stream());\n\n        for await event in events {\n            let completion: Completion = serde_json::from_str(&event)?;\n            content.text.push_str(&completion.delta.content);\n            yield ClientResult::new_ok(content.clone());\n        }\n    };\n    Box::pin(stream)\n}\n```\n\n## Best Practices\n\n1. **Use PlatformSend for cross-platform**: Same code works on native and WASM\n2. **Use spawn() not tokio::spawn**: Platform-agnostic task spawning\n3. **Use AbortOnDropHandle**: Cancel tasks when widget drops\n4. **Use ThreadToken for non-Send on WASM**: Thread-local storage with token access\n5. **Use Slot for custom content**: Allow BotClient to provide custom widgets\n6. **Use read()\u002Fwrite() pattern**: Safe borrow access via WidgetRef\n7. **Use UiRunner::defer_with_redraw**: Update widget from async context\n8. **Handle ClientResult partial success**: May have value AND errors\n\n## Reference Files\n\n- `llms.txt` - Complete MolyKit API reference\n",{"name":5,"risk":20,"source":29,"description":43},"CRITICAL: Use for MolyKit AI chat toolkit. Triggers on:\nBotClient, OpenAI, SSE streaming, AI chat, molykit,\nPlatformSend, spawn(), ThreadToken, cross-platform async,\nChat widget, Messages, PromptInput, Avatar, LLM\n","main","7a38d8e7073e61c16eda85681cb32fb68a8657a5","skills-store\u002Fglobal\u002Fmolykit","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router","https:\u002F\u002Fgithub.com\u002Fsapsapshen","2026-04-14 21:12:12",{"source":8,"skillName":5,"commands":51,"prompt":61,"zipAvailable":62},[52,55,58],{"tool":53,"command":54},"npx","npx skills add sapsapshen\u002Fai-memory-router --skill molykit",{"tool":56,"command":57},"bunx","bunx skills add sapsapshen\u002Fai-memory-router --skill molykit",{"tool":59,"command":60},"pnpm","pnpm dlx skills add sapsapshen\u002Fai-memory-router --skill molykit","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fglobal\u002Fmolykit\u002FSKILL.md",true,[],[65,89,115,125,138,157],{"skillNo":66,"name":67,"nameZh":6,"description":68,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":69,"categories":70,"keywords":76,"publishedAt":38},"01M71ATQRKG48SH06FQ697T5G9","wechat-search","Search WeChat Official Account articles using OpenClaw's web search and fetch capabilities with compliance-focused design.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fwechat-search-release\u002FSKILL.md",[71],{"categoryNo":72,"categoryName":73,"taxonomyType":74,"taxonomyTypeDesc":75},"01JBSEEDCAT000000000000037","律师与法务","PROFESSION","适用职业",[77,78,79,80,81,82,83,84,85,86,87,88],"wechat search","wechat","search","capabilities","openclaw","design","web search","compliance","web","official","fetch","with",{"skillNo":90,"name":91,"nameZh":6,"description":92,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":93,"categories":94,"keywords":95,"publishedAt":38},"01M71ATNAVGZB35PFYM2GC5N00","trend-tap","Real-time trending topics aggregator across 7 platforms (X\u002FTwitter, Reddit, Google Trends, Hacker News, Zhihu, Bilibili, Weibo). Trigger: when user says 'trend', 'trends', 'trend tap', '热点', '热搜', or asks what is trending\u002Fhot. Do NOT use web_search — this skill already fetches live data from all platforms with zero API keys.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftrend-tap\u002FSKILL.md",[],[96,97,98,99,100,101,79,102,103,104,105,106,107,108,109,110,111,112,113,114],"trend","trend tap","tap","api keys","Bash","time","trending","hacker","aggregator","do","when","brew","live","data","google","install","read","across","api",{"skillNo":116,"name":117,"nameZh":6,"description":118,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":119,"categories":120,"keywords":121,"publishedAt":38},"01M71ATJ9AM5KM15AA8PS302EB","trend-radar","Real-time trending topics aggregator across 7 platforms (X\u002FTwitter, Reddit, Google Trends, Hacker News, Zhihu, Bilibili, Weibo). Trigger: when user says 'trend', 'trends', 'trend pulse', '热点', '热搜', or asks what is trending\u002Fhot. Do NOT use web_search — this skill already fetches live data from all platforms with zero API keys.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftrend-radar\u002FSKILL.md",[],[122,123,96,107,108,109,110,100,79,112,124,113,114,106,104,103,102,111,101,99],"trend radar","radar","pulse",{"skillNo":126,"name":127,"nameZh":6,"description":128,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":129,"categories":130,"keywords":131,"publishedAt":38},"01M71ATGF095N3EJM9G8SD0BB5","tech-news-daily","每日科技新闻汇——从全球多个 RSS 源抓取最新科技新闻，翻译（英→中）、去重、排版成每日简报。当用户询问科技新闻、科技新鲜事、每日科技、技术动态、Daily Tech News 或类似表达时触发此技能。","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftech-news-daily\u002FSKILL.md",[],[132,133,134,135,136,137],"news","tech","tech news daily","daily","rss","翻译",{"skillNo":139,"name":140,"nameZh":6,"description":141,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":142,"categories":143,"keywords":149,"publishedAt":38},"01M71ATFAEAMQ17M70NQNY7SR8","summarize","Summarize URLs or files with the summarize CLI (web, PDFs, images, audio, YouTube).","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fsummarize\u002FSKILL.md",[144],{"categoryNo":145,"categoryName":146,"taxonomyType":147,"taxonomyTypeDesc":148},"01JBSEEDCAT000000000000021","资料检索与研究","SCENARIO","使用场景",[140,85,150,151,152,88,107,153,154,155,98,156,111],"Audio","cli","youtube","images","https","files","pdfs",{"skillNo":158,"name":159,"nameZh":6,"description":160,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":161,"categories":162,"keywords":163,"publishedAt":38},"01M71ATDNS3R0M4H936PACQAWZ","sonoscli","Control Sonos speakers (discover\u002Fstatus\u002Fplay\u002Fvolume\u002Fgroup).","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fsonoscli\u002FSKILL.md",[],[159,164,165,166,111,167,154,168,169,170,171,172],"status","play","go","latest","github","discover","control","cmd","volume"]