# Molykit

> |

- **Type:** Skill
- **Install:** `agentstack add skill-sagev1-makepad-skills-molykit`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [SageV1](https://agentstack.voostack.com/s/sagev1)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [SageV1](https://github.com/SageV1)
- **Source:** https://github.com/SageV1/makepad-skills/tree/main/skills/molykit
- **Website:** https://sagev1.github.io

## Install

```sh
agentstack add skill-sagev1-makepad-skills-molykit
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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 = Pin + 'a>>;

/// Boxed stream type for cross-platform use
pub type BoxPlatformSendStream = 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>;

    /// Get available bots/models
    fn bots(&self) -> BoxPlatformSendFuture>>;

    /// 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 =  { text: "P" }
        }
        dependency =  {
            visible: false,
            image =  {}
        }
    }
}

impl Widget for Avatar {
    fn draw_walk(&mut self, cx: &mut Cx2d, ...) -> DrawStep {
        if let Some(avatar) = &self.avatar {
            match avatar {
                Picture::Grapheme(g) => {
                    self.view(id!(grapheme)).set_visible(cx, true);
                    self.view(id!(dependency)).set_visible(cx, false);
                    self.label(id!(label)).set_text(cx, &g);
                }
                Picture::Dependency(d) => {
                    self.view(id!(dependency)).set_visible(cx, true);
                    self.view(id!(grapheme)).set_visible(cx, false);
                    self.image(id!(image)).load_image_dep_by_path(cx, d.as_str());
                }
            }
        }
        self.deref.draw_walk(cx, scope, walk)
    }
}
```

### PromptInput Widget

```rust
#[derive(Live, Widget)]
pub struct PromptInput {
    #[deref] deref: CommandTextInput,
    #[live] pub send_icon: LiveValue,
    #[live] pub stop_icon: LiveValue,
    #[rust] pub task: Task,           // Send or Stop
    #[rust] pub interactivity: Interactivity,
}

impl PromptInput {
    pub fn submitted(&self, actions: &Actions) -> bool;
    pub fn reset(&mut self, cx: &mut Cx);
    pub fn set_send(&mut self);
    pub fn set_stop(&mut self);
    pub fn enable(&mut self);
    pub fn disable(&mut self);
}
```

### Messages Widget - Conversation View

```rust
#[derive(Live, Widget)]
pub struct Messages {
    #[deref] deref: View,
    #[rust] pub messages: Vec,
    #[rust] pub bot_context: Option,
}

impl Messages {
    pub fn set_messages(&mut self, messages: Vec, scroll_to_bottom: bool);
    pub fn scroll_to_bottom(&mut self, cx: &mut Cx, triggered_by_stream: bool);
    pub fn is_at_bottom(&self) -> bool;
}
```

## UiRunner Pattern for Async-to-UI

```rust
impl Widget for PromptInput {
    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
        self.deref.handle_event(cx, event, scope);
        self.ui_runner().handle(cx, event, scope, self);

        if self.button(id!(attach)).clicked(event.actions()) {
            let ui = self.ui_runner();
            Attachment::pick_multiple(move |result| match result {
                Ok(attachments) => {
                    ui.defer_with_redraw(move |me, cx, _| {
                        me.attachment_list_ref().write().attachments.extend(attachments);
                    });
                }
                Err(_) => {}
            });
        }
    }
}
```

## SSE Streaming

```rust
/// Parse SSE byte stream into message stream
pub fn parse_sse(s: S) -> impl Stream>
where
    S: Stream>,
    B: AsRef,
{
    // Split on "\n\n", extract "data:" content
    // Filter comments and [DONE] messages
}

// Usage in BotClient::send
fn send(&mut self, ...) -> BoxPlatformSendStream {
    let stream = stream! {
        let response = client.post(url).send().await?;
        let events = parse_sse(response.bytes_stream());

        for await event in events {
            let completion: Completion = serde_json::from_str(&event)?;
            content.text.push_str(&completion.delta.content);
            yield ClientResult::new_ok(content.clone());
        }
    };
    Box::pin(stream)
}
```

## Best Practices

1. **Use PlatformSend for cross-platform**: Same code works on native and WASM
2. **Use spawn() not tokio::spawn**: Platform-agnostic task spawning
3. **Use AbortOnDropHandle**: Cancel tasks when widget drops
4. **Use ThreadToken for non-Send on WASM**: Thread-local storage with token access
5. **Use Slot for custom content**: Allow BotClient to provide custom widgets
6. **Use read()/write() pattern**: Safe borrow access via WidgetRef
7. **Use UiRunner::defer_with_redraw**: Update widget from async context
8. **Handle ClientResult partial success**: May have value AND errors

## Reference Files

- `llms.txt` - Complete MolyKit API reference

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [SageV1](https://github.com/SageV1)
- **Source:** [SageV1/makepad-skills](https://github.com/SageV1/makepad-skills)
- **License:** MIT
- **Homepage:** https://sagev1.github.io

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** yes
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-sagev1-makepad-skills-molykit
- Seller: https://agentstack.voostack.com/s/sagev1
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
