[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M714EN7EBQAMZGGYVJ9X8EKN":3,"skill-same:01M714EN7EBQAMZGGYVJ9X8EKN":60},{"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":34,"linkOnly":35,"markdownContent":36,"markdownBody":37,"frontMatter":38,"refName":40,"commitSha":41,"skillDirectory":42,"forksCount":14,"repositoryHtmlUrl":43,"repositoryHomepageUrl":6,"ownerHtmlUrl":44,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":45,"install":46,"resources":59},"01M714EN7EBQAMZGGYVJ9X8EKN","robius-matrix-integration",null,"CRITICAL: Use for Matrix SDK integration with Makepad. Triggers on:\nMatrix SDK, sliding sync, MatrixRequest, timeline,\nmatrix-sdk, matrix client, robrix, matrix room,\nMatrix 集成, 聊天客户端","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\u002Fcopilot\u002Frobius-matrix-integration\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33],"integration","robius matrix integration","matrix","sdk","sync","timeline","聊天","use","community","triggers","critical","client","with","for","unknown","2026-11-10 10:12:30",false,"---\nname: robius-matrix-integration\ndescription: |\n  CRITICAL: Use for Matrix SDK integration with Makepad. Triggers on:\n  Matrix SDK, sliding sync, MatrixRequest, timeline,\n  matrix-sdk, matrix client, robrix, matrix room,\n  Matrix 集成, 聊天客户端\nrisk: unknown\nsource: community\n---\n\n# Robius Matrix SDK Integration Skill\n\nBest practices for integrating external APIs with Makepad applications based on Robrix and Moly codebases.\n\n**Source codebases:**\n- **Robrix**: Matrix SDK integration - sliding sync, timeline subscriptions, real-time updates\n- **Moly**: OpenAI\u002FLLM API integration - SSE streaming, MCP protocol, multi-provider support\n\n## Triggers\n\nUse this skill when:\n- Integrating Matrix SDK with Makepad\n- Building a Matrix client with Makepad\n- Implementing Matrix features (rooms, timelines, messages)\n- Handling Matrix SDK async operations in UI\n- Keywords: matrix-sdk, matrix client, robrix, matrix timeline, matrix room, sliding sync\n\n## Overview\n\nRobrix uses the `matrix-sdk` and `matrix-sdk-ui` crates to connect to Matrix homeservers. The key architectural decisions:\n\n1. **Sliding Sync**: Uses native sliding sync for efficient room list updates\n2. **Separate Runtime**: Tokio runtime runs Matrix operations, Makepad handles UI\n3. **Request\u002FResponse Pattern**: UI sends requests, receives actions\u002Fupdates back\n4. **Per-Room Background Tasks**: Each room has dedicated timeline subscriber task\n\n## MatrixRequest Pattern\n\n### Request Enum Definition\n\n```rust\n\u002F\u002F\u002F All async requests that can be made to the Matrix worker task\npub enum MatrixRequest {\n    \u002F\u002F\u002F Login requests\n    Login(LoginRequest),\n    Logout { is_desktop: bool },\n\n    \u002F\u002F\u002F Timeline operations\n    PaginateRoomTimeline {\n        room_id: OwnedRoomId,\n        num_events: u16,\n        direction: PaginationDirection,\n    },\n    SendMessage {\n        room_id: OwnedRoomId,\n        message: RoomMessageEventContent,\n        replied_to: Option\u003CReply>,\n    },\n    EditMessage {\n        room_id: OwnedRoomId,\n        timeline_event_item_id: TimelineEventItemId,\n        edited_content: EditedContent,\n    },\n    RedactMessage {\n        room_id: OwnedRoomId,\n        timeline_event_id: TimelineEventItemId,\n        reason: Option\u003CString>,\n    },\n\n    \u002F\u002F\u002F Room operations\n    JoinRoom { room_id: OwnedRoomId },\n    LeaveRoom { room_id: OwnedRoomId },\n    GetRoomMembers {\n        room_id: OwnedRoomId,\n        memberships: RoomMemberships,\n        local_only: bool,\n    },\n\n    \u002F\u002F\u002F User operations\n    GetUserProfile {\n        user_id: OwnedUserId,\n        room_id: Option\u003COwnedRoomId>,\n        local_only: bool,\n    },\n    IgnoreUser {\n        ignore: bool,\n        room_member: RoomMember,\n        room_id: OwnedRoomId,\n    },\n\n    \u002F\u002F\u002F Media operations\n    FetchAvatar {\n        mxc_uri: OwnedMxcUri,\n        on_fetched: fn(AvatarUpdate),\n    },\n    FetchMedia {\n        media_request: MediaRequestParameters,\n        on_fetched: OnMediaFetchedFn,\n        destination: MediaCacheEntryRef,\n        update_sender: Option\u003Ccrossbeam_channel::Sender\u003CTimelineUpdate>>,\n    },\n\n    \u002F\u002F\u002F Typing\u002Fread indicators\n    SendTypingNotice { room_id: OwnedRoomId, typing: bool },\n    ReadReceipt { room_id: OwnedRoomId, event_id: OwnedEventId },\n    FullyReadReceipt { room_id: OwnedRoomId, event_id: OwnedEventId },\n\n    \u002F\u002F\u002F Reactions\n    ToggleReaction {\n        room_id: OwnedRoomId,\n        timeline_event_id: TimelineEventItemId,\n        reaction: String,\n    },\n\n    \u002F\u002F\u002F Subscriptions\n    SubscribeToTypingNotices { room_id: OwnedRoomId, subscribe: bool },\n    SubscribeToPinnedEvents { room_id: OwnedRoomId, subscribe: bool },\n}\n```\n\n### Submit Pattern\n\n```rust\nstatic REQUEST_SENDER: Mutex\u003COption\u003CUnboundedSender\u003CMatrixRequest>>> = Mutex::new(None);\n\n\u002F\u002F\u002F Submit request from UI thread to async runtime\npub fn submit_async_request(req: MatrixRequest) {\n    if let Some(sender) = REQUEST_SENDER.lock().unwrap().as_ref() {\n        sender.send(req).expect(\"BUG: matrix worker task receiver died!\");\n    }\n}\n\n\u002F\u002F Usage in UI\nsubmit_async_request(MatrixRequest::SendMessage {\n    room_id: room_id.clone(),\n    message: RoomMessageEventContent::text_plain(&text),\n    replied_to: self.reply_to.take(),\n});\n```\n\n## Worker Task Handler\n\n```rust\nasync fn matrix_worker_task(\n    mut request_receiver: UnboundedReceiver\u003CMatrixRequest>,\n    login_sender: Sender\u003CLoginRequest>,\n) -> Result\u003C()> {\n    while let Some(request) = request_receiver.recv().await {\n        match request {\n            MatrixRequest::PaginateRoomTimeline { room_id, num_events, direction } => {\n                let (timeline, sender) = {\n                    let rooms = ALL_JOINED_ROOMS.lock().unwrap();\n                    let Some(room_info) = rooms.get(&room_id) else {\n                        continue;  \u002F\u002F Room not ready yet\n                    };\n                    (room_info.timeline.clone(), room_info.update_sender.clone())\n                };\n\n                \u002F\u002F Spawn dedicated task for this operation\n                Handle::current().spawn(async move {\n                    \u002F\u002F Notify UI pagination is starting\n                    sender.send(TimelineUpdate::PaginationRunning(direction)).unwrap();\n                    SignalToUI::set_ui_signal();\n\n                    \u002F\u002F Perform pagination\n                    let res = if direction == PaginationDirection::Forwards {\n                        timeline.paginate_forwards(num_events).await\n                    } else {\n                        timeline.paginate_backwards(num_events).await\n                    };\n\n                    \u002F\u002F Send result to UI\n                    match res {\n                        Ok(fully_paginated) => {\n                            sender.send(TimelineUpdate::PaginationIdle {\n                                fully_paginated,\n                                direction,\n                            }).unwrap();\n                        }\n                        Err(error) => {\n                            sender.send(TimelineUpdate::PaginationError {\n                                error,\n                                direction,\n                            }).unwrap();\n                        }\n                    }\n                    SignalToUI::set_ui_signal();\n                });\n            }\n\n            MatrixRequest::JoinRoom { room_id } => {\n                let Some(client) = get_client() else { continue };\n\n                Handle::current().spawn(async move {\n                    let result_action = if let Some(room) = client.get_room(&room_id) {\n                        match room.join().await {\n                            Ok(()) => JoinRoomResultAction::Joined { room_id },\n                            Err(e) => JoinRoomResultAction::Failed { room_id, error: e },\n                        }\n                    } else {\n                        match client.join_room_by_id(&room_id).await {\n                            Ok(_) => JoinRoomResultAction::Joined { room_id },\n                            Err(e) => JoinRoomResultAction::Failed { room_id, error: e },\n                        }\n                    };\n                    Cx::post_action(result_action);\n                });\n            }\n            \u002F\u002F ... handle other requests\n        }\n    }\n    Ok(())\n}\n```\n\n## Timeline Updates\n\n### TimelineUpdate Enum\n\n```rust\npub enum TimelineUpdate {\n    \u002F\u002F\u002F New items added to timeline\n    NewItems {\n        new_items: Vector\u003CArc\u003CTimelineItem>>,\n        changed_indices: BTreeSet\u003Cusize>,\n        is_append: bool,\n    },\n    \u002F\u002F\u002F Pagination state changes\n    PaginationRunning(PaginationDirection),\n    PaginationIdle {\n        fully_paginated: bool,\n        direction: PaginationDirection,\n    },\n    PaginationError {\n        error: Error,\n        direction: PaginationDirection,\n    },\n    \u002F\u002F\u002F Message edit result\n    MessageEdited {\n        timeline_event_id: TimelineEventItemId,\n        result: Result\u003C(), Error>,\n    },\n    \u002F\u002F\u002F Room members fetched\n    RoomMembersListFetched {\n        members: Vec\u003CRoomMember>,\n        sort: PrecomputedMemberSort,\n        is_local_fetch: bool,\n    },\n    \u002F\u002F\u002F Unread count updated\n    NewUnreadMessagesCount(UnreadMessageCount),\n    \u002F\u002F\u002F User power levels fetched\n    UserPowerLevels(UserPowerLevels),\n}\n```\n\n### Per-Room Update Flow\n\n```rust\nstruct JoinedRoomDetails {\n    room_id: OwnedRoomId,\n    timeline: Arc\u003CTimeline>,\n    timeline_update_sender: crossbeam_channel::Sender\u003CTimelineUpdate>,\n    timeline_subscriber_handler_task: JoinHandle\u003C()>,\n    typing_notice_subscriber: Option\u003CEventHandlerDropGuard>,\n}\n\nimpl Drop for JoinedRoomDetails {\n    fn drop(&mut self) {\n        \u002F\u002F Cleanup background tasks when room closes\n        self.timeline_subscriber_handler_task.abort();\n        drop(self.typing_notice_subscriber.take());\n    }\n}\n\n\u002F\u002F Spawn subscriber for a room\nasync fn spawn_timeline_subscriber(\n    room_id: OwnedRoomId,\n    timeline: Arc\u003CTimeline>,\n    sender: crossbeam_channel::Sender\u003CTimelineUpdate>,\n) -> JoinHandle\u003C()> {\n    tokio::spawn(async move {\n        let (items, mut stream) = timeline.subscribe().await;\n\n        \u002F\u002F Send initial items\n        sender.send(TimelineUpdate::NewItems {\n            new_items: items,\n            changed_indices: BTreeSet::new(),\n            is_append: false,\n        }).unwrap();\n        SignalToUI::set_ui_signal();\n\n        \u002F\u002F Listen for updates\n        while let Some(diff) = stream.next().await {\n            let update = process_timeline_diff(diff);\n            sender.send(update).unwrap();\n            SignalToUI::set_ui_signal();\n        }\n    })\n}\n```\n\n### Handling Updates in UI\n\n```rust\nimpl Widget for RoomScreen {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {\n        \u002F\u002F Poll timeline updates on Signal events\n        if let Event::Signal = event {\n            while let Ok(update) = self.timeline_state.update_receiver.try_recv() {\n                match update {\n                    TimelineUpdate::NewItems { new_items, changed_indices, is_append } => {\n                        self.apply_new_items(cx, new_items, changed_indices, is_append);\n                    }\n                    TimelineUpdate::PaginationIdle { fully_paginated, direction } => {\n                        self.set_pagination_idle(cx, direction, fully_paginated);\n                    }\n                    TimelineUpdate::PaginationError { error, direction } => {\n                        self.show_pagination_error(cx, direction, &error);\n                    }\n                    \u002F\u002F ... handle other updates\n                }\n            }\n        }\n\n        self.view.handle_event(cx, event, scope);\n    }\n}\n```\n\n## Room List Updates\n\n### RoomsListUpdate Enum\n\n```rust\npub enum RoomsListUpdate {\n    NotLoaded,\n    LoadedRooms { max_rooms: Option\u003Cu32> },\n    AddInvitedRoom(InvitedRoomInfo),\n    AddJoinedRoom(JoinedRoomInfo),\n    ClearRooms,\n    UpdateLatestEvent {\n        room_id: OwnedRoomId,\n        timestamp: MilliSecondsSinceUnixEpoch,\n        latest_message_text: String,\n    },\n    UpdateNumUnreadMessages {\n        room_id: OwnedRoomId,\n        unread_messages: UnreadMessageCount,\n        unread_mentions: u64,\n    },\n    UpdateRoomName { new_room_name: RoomNameId },\n    UpdateRoomAvatar { room_id: OwnedRoomId, avatar: FetchedRoomAvatar },\n    RemoveRoom { room_id: OwnedRoomId, new_state: RoomState },\n    Status { status: String },\n    ScrollToRoom(OwnedRoomId),\n}\n\nstatic PENDING_ROOM_UPDATES: SegQueue\u003CRoomsListUpdate> = SegQueue::new();\n\npub fn enqueue_rooms_list_update(update: RoomsListUpdate) {\n    PENDING_ROOM_UPDATES.push(update);\n    SignalToUI::set_ui_signal();\n}\n```\n\n## Client Build Pattern\n\n```rust\nasync fn build_client(\n    homeserver_url: &str,\n    data_dir: &Path,\n) -> Result\u003C(Client, ClientSessionPersisted)> {\n    \u002F\u002F Generate unique subfolder for this session\n    let db_subfolder = format!(\"db_{}\", chrono::Local::now().format(\"%F_%H_%M_%S_%f\"));\n    let db_path = data_dir.join(db_subfolder);\n\n    \u002F\u002F Generate random passphrase for encryption\n    let passphrase: String = {\n        use rand::{Rng, thread_rng};\n        thread_rng()\n            .sample_iter(rand::distributions::Alphanumeric)\n            .take(32)\n            .map(char::from)\n            .collect()\n    };\n\n    let client = Client::builder()\n        .server_name_or_homeserver_url(homeserver_url)\n        .sqlite_store(&db_path, Some(&passphrase))\n        .sliding_sync_version_builder(VersionBuilder::DiscoverNative)\n        .with_decryption_settings(DecryptionSettings {\n            sender_device_trust_requirement: TrustRequirement::Untrusted,\n        })\n        .with_encryption_settings(EncryptionSettings {\n            auto_enable_cross_signing: true,\n            backup_download_strategy: BackupDownloadStrategy::OneShot,\n            auto_enable_backups: true,\n        })\n        .request_config(\n            RequestConfig::new().timeout(Duration::from_secs(60))\n        )\n        .build()\n        .await?;\n\n    Ok((client, ClientSessionPersisted { homeserver: homeserver_url.to_string(), db_path, passphrase }))\n}\n```\n\n## Best Practices\n\n1. **Always spawn tasks**: Don't block the worker task receiver loop\n2. **Use crossbeam channels for per-room updates**: More efficient than global queue\n3. **Always call SignalToUI::set_ui_signal()**: After enqueueing any update\n4. **Handle room not ready**: Skip requests for rooms not yet in `ALL_JOINED_ROOMS`\n5. **Cleanup on drop**: Abort background tasks when rooms are closed\n6. **Use Cx::post_action for results**: Posted actions are handled in App::handle_actions\n7. **Use SegQueue for high-frequency updates**: Lock-free for room list updates\n\n## Reference Files\n\n- `references\u002Fmatrix-client.md` - Matrix client setup and login patterns (Robrix)\n- `references\u002Ftimeline-handling.md` - Matrix timeline subscription patterns (Robrix)\n- `references\u002Fmoly-api-integration.md` - Moly API integration patterns\n  - OpenAI client with SSE streaming\n  - Platform-agnostic async streams\n  - MCP (Model Context Protocol) integration\n  - Tool approval flow\n  - MolyClient for local server\n  - BotContext for multi-provider support\n","# Robius Matrix SDK Integration Skill\n\nBest practices for integrating external APIs with Makepad applications based on Robrix and Moly codebases.\n\n**Source codebases:**\n- **Robrix**: Matrix SDK integration - sliding sync, timeline subscriptions, real-time updates\n- **Moly**: OpenAI\u002FLLM API integration - SSE streaming, MCP protocol, multi-provider support\n\n## Triggers\n\nUse this skill when:\n- Integrating Matrix SDK with Makepad\n- Building a Matrix client with Makepad\n- Implementing Matrix features (rooms, timelines, messages)\n- Handling Matrix SDK async operations in UI\n- Keywords: matrix-sdk, matrix client, robrix, matrix timeline, matrix room, sliding sync\n\n## Overview\n\nRobrix uses the `matrix-sdk` and `matrix-sdk-ui` crates to connect to Matrix homeservers. The key architectural decisions:\n\n1. **Sliding Sync**: Uses native sliding sync for efficient room list updates\n2. **Separate Runtime**: Tokio runtime runs Matrix operations, Makepad handles UI\n3. **Request\u002FResponse Pattern**: UI sends requests, receives actions\u002Fupdates back\n4. **Per-Room Background Tasks**: Each room has dedicated timeline subscriber task\n\n## MatrixRequest Pattern\n\n### Request Enum Definition\n\n```rust\n\u002F\u002F\u002F All async requests that can be made to the Matrix worker task\npub enum MatrixRequest {\n    \u002F\u002F\u002F Login requests\n    Login(LoginRequest),\n    Logout { is_desktop: bool },\n\n    \u002F\u002F\u002F Timeline operations\n    PaginateRoomTimeline {\n        room_id: OwnedRoomId,\n        num_events: u16,\n        direction: PaginationDirection,\n    },\n    SendMessage {\n        room_id: OwnedRoomId,\n        message: RoomMessageEventContent,\n        replied_to: Option\u003CReply>,\n    },\n    EditMessage {\n        room_id: OwnedRoomId,\n        timeline_event_item_id: TimelineEventItemId,\n        edited_content: EditedContent,\n    },\n    RedactMessage {\n        room_id: OwnedRoomId,\n        timeline_event_id: TimelineEventItemId,\n        reason: Option\u003CString>,\n    },\n\n    \u002F\u002F\u002F Room operations\n    JoinRoom { room_id: OwnedRoomId },\n    LeaveRoom { room_id: OwnedRoomId },\n    GetRoomMembers {\n        room_id: OwnedRoomId,\n        memberships: RoomMemberships,\n        local_only: bool,\n    },\n\n    \u002F\u002F\u002F User operations\n    GetUserProfile {\n        user_id: OwnedUserId,\n        room_id: Option\u003COwnedRoomId>,\n        local_only: bool,\n    },\n    IgnoreUser {\n        ignore: bool,\n        room_member: RoomMember,\n        room_id: OwnedRoomId,\n    },\n\n    \u002F\u002F\u002F Media operations\n    FetchAvatar {\n        mxc_uri: OwnedMxcUri,\n        on_fetched: fn(AvatarUpdate),\n    },\n    FetchMedia {\n        media_request: MediaRequestParameters,\n        on_fetched: OnMediaFetchedFn,\n        destination: MediaCacheEntryRef,\n        update_sender: Option\u003Ccrossbeam_channel::Sender\u003CTimelineUpdate>>,\n    },\n\n    \u002F\u002F\u002F Typing\u002Fread indicators\n    SendTypingNotice { room_id: OwnedRoomId, typing: bool },\n    ReadReceipt { room_id: OwnedRoomId, event_id: OwnedEventId },\n    FullyReadReceipt { room_id: OwnedRoomId, event_id: OwnedEventId },\n\n    \u002F\u002F\u002F Reactions\n    ToggleReaction {\n        room_id: OwnedRoomId,\n        timeline_event_id: TimelineEventItemId,\n        reaction: String,\n    },\n\n    \u002F\u002F\u002F Subscriptions\n    SubscribeToTypingNotices { room_id: OwnedRoomId, subscribe: bool },\n    SubscribeToPinnedEvents { room_id: OwnedRoomId, subscribe: bool },\n}\n```\n\n### Submit Pattern\n\n```rust\nstatic REQUEST_SENDER: Mutex\u003COption\u003CUnboundedSender\u003CMatrixRequest>>> = Mutex::new(None);\n\n\u002F\u002F\u002F Submit request from UI thread to async runtime\npub fn submit_async_request(req: MatrixRequest) {\n    if let Some(sender) = REQUEST_SENDER.lock().unwrap().as_ref() {\n        sender.send(req).expect(\"BUG: matrix worker task receiver died!\");\n    }\n}\n\n\u002F\u002F Usage in UI\nsubmit_async_request(MatrixRequest::SendMessage {\n    room_id: room_id.clone(),\n    message: RoomMessageEventContent::text_plain(&text),\n    replied_to: self.reply_to.take(),\n});\n```\n\n## Worker Task Handler\n\n```rust\nasync fn matrix_worker_task(\n    mut request_receiver: UnboundedReceiver\u003CMatrixRequest>,\n    login_sender: Sender\u003CLoginRequest>,\n) -> Result\u003C()> {\n    while let Some(request) = request_receiver.recv().await {\n        match request {\n            MatrixRequest::PaginateRoomTimeline { room_id, num_events, direction } => {\n                let (timeline, sender) = {\n                    let rooms = ALL_JOINED_ROOMS.lock().unwrap();\n                    let Some(room_info) = rooms.get(&room_id) else {\n                        continue;  \u002F\u002F Room not ready yet\n                    };\n                    (room_info.timeline.clone(), room_info.update_sender.clone())\n                };\n\n                \u002F\u002F Spawn dedicated task for this operation\n                Handle::current().spawn(async move {\n                    \u002F\u002F Notify UI pagination is starting\n                    sender.send(TimelineUpdate::PaginationRunning(direction)).unwrap();\n                    SignalToUI::set_ui_signal();\n\n                    \u002F\u002F Perform pagination\n                    let res = if direction == PaginationDirection::Forwards {\n                        timeline.paginate_forwards(num_events).await\n                    } else {\n                        timeline.paginate_backwards(num_events).await\n                    };\n\n                    \u002F\u002F Send result to UI\n                    match res {\n                        Ok(fully_paginated) => {\n                            sender.send(TimelineUpdate::PaginationIdle {\n                                fully_paginated,\n                                direction,\n                            }).unwrap();\n                        }\n                        Err(error) => {\n                            sender.send(TimelineUpdate::PaginationError {\n                                error,\n                                direction,\n                            }).unwrap();\n                        }\n                    }\n                    SignalToUI::set_ui_signal();\n                });\n            }\n\n            MatrixRequest::JoinRoom { room_id } => {\n                let Some(client) = get_client() else { continue };\n\n                Handle::current().spawn(async move {\n                    let result_action = if let Some(room) = client.get_room(&room_id) {\n                        match room.join().await {\n                            Ok(()) => JoinRoomResultAction::Joined { room_id },\n                            Err(e) => JoinRoomResultAction::Failed { room_id, error: e },\n                        }\n                    } else {\n                        match client.join_room_by_id(&room_id).await {\n                            Ok(_) => JoinRoomResultAction::Joined { room_id },\n                            Err(e) => JoinRoomResultAction::Failed { room_id, error: e },\n                        }\n                    };\n                    Cx::post_action(result_action);\n                });\n            }\n            \u002F\u002F ... handle other requests\n        }\n    }\n    Ok(())\n}\n```\n\n## Timeline Updates\n\n### TimelineUpdate Enum\n\n```rust\npub enum TimelineUpdate {\n    \u002F\u002F\u002F New items added to timeline\n    NewItems {\n        new_items: Vector\u003CArc\u003CTimelineItem>>,\n        changed_indices: BTreeSet\u003Cusize>,\n        is_append: bool,\n    },\n    \u002F\u002F\u002F Pagination state changes\n    PaginationRunning(PaginationDirection),\n    PaginationIdle {\n        fully_paginated: bool,\n        direction: PaginationDirection,\n    },\n    PaginationError {\n        error: Error,\n        direction: PaginationDirection,\n    },\n    \u002F\u002F\u002F Message edit result\n    MessageEdited {\n        timeline_event_id: TimelineEventItemId,\n        result: Result\u003C(), Error>,\n    },\n    \u002F\u002F\u002F Room members fetched\n    RoomMembersListFetched {\n        members: Vec\u003CRoomMember>,\n        sort: PrecomputedMemberSort,\n        is_local_fetch: bool,\n    },\n    \u002F\u002F\u002F Unread count updated\n    NewUnreadMessagesCount(UnreadMessageCount),\n    \u002F\u002F\u002F User power levels fetched\n    UserPowerLevels(UserPowerLevels),\n}\n```\n\n### Per-Room Update Flow\n\n```rust\nstruct JoinedRoomDetails {\n    room_id: OwnedRoomId,\n    timeline: Arc\u003CTimeline>,\n    timeline_update_sender: crossbeam_channel::Sender\u003CTimelineUpdate>,\n    timeline_subscriber_handler_task: JoinHandle\u003C()>,\n    typing_notice_subscriber: Option\u003CEventHandlerDropGuard>,\n}\n\nimpl Drop for JoinedRoomDetails {\n    fn drop(&mut self) {\n        \u002F\u002F Cleanup background tasks when room closes\n        self.timeline_subscriber_handler_task.abort();\n        drop(self.typing_notice_subscriber.take());\n    }\n}\n\n\u002F\u002F Spawn subscriber for a room\nasync fn spawn_timeline_subscriber(\n    room_id: OwnedRoomId,\n    timeline: Arc\u003CTimeline>,\n    sender: crossbeam_channel::Sender\u003CTimelineUpdate>,\n) -> JoinHandle\u003C()> {\n    tokio::spawn(async move {\n        let (items, mut stream) = timeline.subscribe().await;\n\n        \u002F\u002F Send initial items\n        sender.send(TimelineUpdate::NewItems {\n            new_items: items,\n            changed_indices: BTreeSet::new(),\n            is_append: false,\n        }).unwrap();\n        SignalToUI::set_ui_signal();\n\n        \u002F\u002F Listen for updates\n        while let Some(diff) = stream.next().await {\n            let update = process_timeline_diff(diff);\n            sender.send(update).unwrap();\n            SignalToUI::set_ui_signal();\n        }\n    })\n}\n```\n\n### Handling Updates in UI\n\n```rust\nimpl Widget for RoomScreen {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {\n        \u002F\u002F Poll timeline updates on Signal events\n        if let Event::Signal = event {\n            while let Ok(update) = self.timeline_state.update_receiver.try_recv() {\n                match update {\n                    TimelineUpdate::NewItems { new_items, changed_indices, is_append } => {\n                        self.apply_new_items(cx, new_items, changed_indices, is_append);\n                    }\n                    TimelineUpdate::PaginationIdle { fully_paginated, direction } => {\n                        self.set_pagination_idle(cx, direction, fully_paginated);\n                    }\n                    TimelineUpdate::PaginationError { error, direction } => {\n                        self.show_pagination_error(cx, direction, &error);\n                    }\n                    \u002F\u002F ... handle other updates\n                }\n            }\n        }\n\n        self.view.handle_event(cx, event, scope);\n    }\n}\n```\n\n## Room List Updates\n\n### RoomsListUpdate Enum\n\n```rust\npub enum RoomsListUpdate {\n    NotLoaded,\n    LoadedRooms { max_rooms: Option\u003Cu32> },\n    AddInvitedRoom(InvitedRoomInfo),\n    AddJoinedRoom(JoinedRoomInfo),\n    ClearRooms,\n    UpdateLatestEvent {\n        room_id: OwnedRoomId,\n        timestamp: MilliSecondsSinceUnixEpoch,\n        latest_message_text: String,\n    },\n    UpdateNumUnreadMessages {\n        room_id: OwnedRoomId,\n        unread_messages: UnreadMessageCount,\n        unread_mentions: u64,\n    },\n    UpdateRoomName { new_room_name: RoomNameId },\n    UpdateRoomAvatar { room_id: OwnedRoomId, avatar: FetchedRoomAvatar },\n    RemoveRoom { room_id: OwnedRoomId, new_state: RoomState },\n    Status { status: String },\n    ScrollToRoom(OwnedRoomId),\n}\n\nstatic PENDING_ROOM_UPDATES: SegQueue\u003CRoomsListUpdate> = SegQueue::new();\n\npub fn enqueue_rooms_list_update(update: RoomsListUpdate) {\n    PENDING_ROOM_UPDATES.push(update);\n    SignalToUI::set_ui_signal();\n}\n```\n\n## Client Build Pattern\n\n```rust\nasync fn build_client(\n    homeserver_url: &str,\n    data_dir: &Path,\n) -> Result\u003C(Client, ClientSessionPersisted)> {\n    \u002F\u002F Generate unique subfolder for this session\n    let db_subfolder = format!(\"db_{}\", chrono::Local::now().format(\"%F_%H_%M_%S_%f\"));\n    let db_path = data_dir.join(db_subfolder);\n\n    \u002F\u002F Generate random passphrase for encryption\n    let passphrase: String = {\n        use rand::{Rng, thread_rng};\n        thread_rng()\n            .sample_iter(rand::distributions::Alphanumeric)\n            .take(32)\n            .map(char::from)\n            .collect()\n    };\n\n    let client = Client::builder()\n        .server_name_or_homeserver_url(homeserver_url)\n        .sqlite_store(&db_path, Some(&passphrase))\n        .sliding_sync_version_builder(VersionBuilder::DiscoverNative)\n        .with_decryption_settings(DecryptionSettings {\n            sender_device_trust_requirement: TrustRequirement::Untrusted,\n        })\n        .with_encryption_settings(EncryptionSettings {\n            auto_enable_cross_signing: true,\n            backup_download_strategy: BackupDownloadStrategy::OneShot,\n            auto_enable_backups: true,\n        })\n        .request_config(\n            RequestConfig::new().timeout(Duration::from_secs(60))\n        )\n        .build()\n        .await?;\n\n    Ok((client, ClientSessionPersisted { homeserver: homeserver_url.to_string(), db_path, passphrase }))\n}\n```\n\n## Best Practices\n\n1. **Always spawn tasks**: Don't block the worker task receiver loop\n2. **Use crossbeam channels for per-room updates**: More efficient than global queue\n3. **Always call SignalToUI::set_ui_signal()**: After enqueueing any update\n4. **Handle room not ready**: Skip requests for rooms not yet in `ALL_JOINED_ROOMS`\n5. **Cleanup on drop**: Abort background tasks when rooms are closed\n6. **Use Cx::post_action for results**: Posted actions are handled in App::handle_actions\n7. **Use SegQueue for high-frequency updates**: Lock-free for room list updates\n\n## Reference Files\n\n- `references\u002Fmatrix-client.md` - Matrix client setup and login patterns (Robrix)\n- `references\u002Ftimeline-handling.md` - Matrix timeline subscription patterns (Robrix)\n- `references\u002Fmoly-api-integration.md` - Moly API integration patterns\n  - OpenAI client with SSE streaming\n  - Platform-agnostic async streams\n  - MCP (Model Context Protocol) integration\n  - Tool approval flow\n  - MolyClient for local server\n  - BotContext for multi-provider support\n",{"name":5,"risk":33,"source":27,"description":39},"CRITICAL: Use for Matrix SDK integration with Makepad. Triggers on:\nMatrix SDK, sliding sync, MatrixRequest, timeline,\nmatrix-sdk, matrix client, robrix, matrix room,\nMatrix 集成, 聊天客户端\n","main","7a38d8e7073e61c16eda85681cb32fb68a8657a5","skills-store\u002Fcopilot\u002Frobius-matrix-integration","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router","https:\u002F\u002Fgithub.com\u002Fsapsapshen","2026-04-14 21:12:12",{"source":8,"skillName":5,"commands":47,"prompt":57,"zipAvailable":58},[48,51,54],{"tool":49,"command":50},"npx","npx skills add sapsapshen\u002Fai-memory-router --skill robius-matrix-integration",{"tool":52,"command":53},"bunx","bunx skills add sapsapshen\u002Fai-memory-router --skill robius-matrix-integration",{"tool":55,"command":56},"pnpm","pnpm dlx skills add sapsapshen\u002Fai-memory-router --skill robius-matrix-integration","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcopilot\u002Frobius-matrix-integration\u002FSKILL.md",true,[],[61,84,110,120,133,152],{"skillNo":62,"name":63,"nameZh":6,"description":64,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":65,"categories":66,"keywords":72,"publishedAt":34},"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",[67],{"categoryNo":68,"categoryName":69,"taxonomyType":70,"taxonomyTypeDesc":71},"01JBSEEDCAT000000000000037","律师与法务","PROFESSION","适用职业",[73,74,75,76,77,78,79,80,81,82,83,31],"wechat search","wechat","search","capabilities","openclaw","design","web search","compliance","web","official","fetch",{"skillNo":85,"name":86,"nameZh":6,"description":87,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":88,"categories":89,"keywords":90,"publishedAt":34},"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",[],[91,92,93,94,95,96,75,97,98,99,100,101,102,103,104,105,106,107,108,109],"trend","trend tap","tap","api keys","Bash","time","trending","hacker","aggregator","do","when","brew","live","data","google","install","read","across","api",{"skillNo":111,"name":112,"nameZh":6,"description":113,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":114,"categories":115,"keywords":116,"publishedAt":34},"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",[],[117,118,91,102,103,104,105,95,75,107,119,108,109,101,99,98,97,106,96,94],"trend radar","radar","pulse",{"skillNo":121,"name":122,"nameZh":6,"description":123,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":124,"categories":125,"keywords":126,"publishedAt":34},"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",[],[127,128,129,130,131,132],"news","tech","tech news daily","daily","rss","翻译",{"skillNo":134,"name":135,"nameZh":6,"description":136,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":137,"categories":138,"keywords":144,"publishedAt":34},"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",[139],{"categoryNo":140,"categoryName":141,"taxonomyType":142,"taxonomyTypeDesc":143},"01JBSEEDCAT000000000000021","资料检索与研究","SCENARIO","使用场景",[135,81,145,146,147,31,102,148,149,150,93,151,106],"Audio","cli","youtube","images","https","files","pdfs",{"skillNo":153,"name":154,"nameZh":6,"description":155,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":156,"categories":157,"keywords":158,"publishedAt":34},"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",[],[154,159,160,161,106,162,149,163,164,165,166,167],"status","play","go","latest","github","discover","control","cmd","volume"]