[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6CVFSS71MKFK1JRJ33TYCW4":3,"skill-same:01M6CVFSS71MKFK1JRJ33TYCW4":59},{"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":30,"linkOnly":31,"markdownContent":32,"markdownBody":33,"frontMatter":34,"refName":37,"commitSha":38,"skillDirectory":39,"forksCount":40,"repositoryHtmlUrl":41,"repositoryHomepageUrl":41,"ownerHtmlUrl":42,"ownerBlogUrl":6,"ownerDescription":43,"updatedAt":44,"install":45,"resources":58},"01M6CVFSS71MKFK1JRJ33TYCW4","robius-state-management",null,"CRITICAL: Use for Robius state management patterns. Triggers on:\nAppState, persistence, theme switch, 状态管理,\nScope::with_data, save state, load state, serde,\n状态持久化, 主题切换","Sam-06060\u002Fjarvis-assistant","Sam-06060","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F208600848?v=4","01M6CRKT7C7FXD21V6HXF293S0","Samson Frank Ganta","01M6CRKTBEKYFD8YX5Z1GT99EE",6,"Apache-2.0","https:\u002F\u002Fgithub.com\u002FSam-06060\u002Fjarvis-assistant\u002Fblob\u002Fmain\u002Fmodules\u002Fagent_skills\u002Fawesome-skills\u002Fplaybooks\u002Frobius-state-management\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29],"state management","state","management","robius state management","persistence","data","save","use","community","switch","patterns","2026-11-10 10:12:30",false,"---\nname: robius-state-management\ndescription: |\n  CRITICAL: Use for Robius state management patterns. Triggers on:\n  AppState, persistence, theme switch, 状态管理,\n  Scope::with_data, save state, load state, serde,\n  状态持久化, 主题切换\nrisk: unknown\nsource: community\n---\n\n# Robius State Management Skill\n\nBest practices for state management and persistence in Makepad applications based on Robrix and Moly codebases.\n\n**Source codebases:**\n- **Robrix**: Matrix chat client - AppState, SelectedRoom, persistence via serde\n- **Moly**: AI chat application - Central Store pattern, async initialization, Preferences\n\n## When to Use\n\nUse this skill when:\n- Designing application state structure\n- Implementing state persistence\n- Passing state through widget tree\n- Managing UI state across sessions\n- Keywords: app state, makepad state, persistence, Scope::with_data, save state, load state\n\n## Production Patterns\n\nFor production-ready state management patterns, see the `_base\u002F` directory:\n\n| Pattern | Description |\n|---------|-------------|\n| 06-global-registry | Global widget registry with Cx::set_global |\n| 07-radio-navigation | Tab-style navigation with radio buttons |\n| 10-state-machine | Enum-based state machine widgets |\n| 11-theme-switching | Multi-theme support with apply_over |\n| 12-local-persistence | Save\u002Fload user preferences |\n\n## AppState Structure\n\n### Core State Definition\n\n```rust\nuse serde::{Serialize, Deserialize};\nuse std::collections::HashMap;\nuse matrix_sdk::ruma::OwnedRoomId;\n\n\u002F\u002F\u002F App-wide state that is stored persistently across multiple app runs\n\u002F\u002F\u002F and shared\u002Fupdated across various parts of the app.\n#[derive(Clone, Default, Debug, Serialize, Deserialize)]\npub struct AppState {\n    \u002F\u002F\u002F The currently-selected room\n    pub selected_room: Option\u003CSelectedRoom>,\n\n    \u002F\u002F\u002F Saved UI layout state for main view\n    pub saved_layout_state: SavedLayoutState,\n\n    \u002F\u002F\u002F Per-item saved states (e.g., per-space dock layouts)\n    pub saved_state_per_item: HashMap\u003COwnedRoomId, SavedLayoutState>,\n\n    \u002F\u002F\u002F Whether a user is currently logged in\n    #[serde(skip)]  \u002F\u002F Don't persist login state\n    pub logged_in: bool,\n}\n\n\u002F\u002F\u002F Represents a currently selected item\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub enum SelectedRoom {\n    JoinedRoom { room_name_id: RoomNameId },\n    InvitedRoom { room_name_id: RoomNameId },\n    Space { space_name_id: RoomNameId },\n}\n\nimpl SelectedRoom {\n    pub fn room_id(&self) -> &OwnedRoomId {\n        match self {\n            Self::JoinedRoom { room_name_id } => room_name_id.room_id(),\n            Self::InvitedRoom { room_name_id } => room_name_id.room_id(),\n            Self::Space { space_name_id } => space_name_id.room_id(),\n        }\n    }\n\n    \u002F\u002F\u002F Upgrade from invited to joined state\n    pub fn upgrade_invite_to_joined(&mut self, room_id: &RoomId) -> bool {\n        match self {\n            Self::InvitedRoom { room_name_id } if room_name_id.room_id() == room_id => {\n                let name = room_name_id.clone();\n                *self = Self::JoinedRoom { room_name_id: name };\n                true\n            }\n            _ => false,\n        }\n    }\n}\n\n\u002F\u002F Equality based on room_id only\nimpl PartialEq for SelectedRoom {\n    fn eq(&self, other: &Self) -> bool {\n        self.room_id() == other.room_id()\n    }\n}\nimpl Eq for SelectedRoom {}\n```\n\n### Layout\u002FDock State Persistence\n\n```rust\n\u002F\u002F\u002F A snapshot of UI layout state for restoration\n#[derive(Clone, Default, Debug, Serialize, Deserialize)]\npub struct SavedLayoutState {\n    \u002F\u002F\u002F All items contained in the layout, keyed by ID\n    pub layout_items: HashMap\u003CLiveIdSerde, LayoutItemSerde>,\n\n    \u002F\u002F\u002F Items currently open, keyed by ID\n    pub open_items: HashMap\u003CLiveIdSerde, SelectedRoom>,\n\n    \u002F\u002F\u002F Order items were opened (chronological)\n    pub item_order: Vec\u003CSelectedRoom>,\n\n    \u002F\u002F\u002F Currently selected item when state was saved\n    pub selected_item: Option\u003CSelectedRoom>,\n}\n\n\u002F\u002F\u002F Serializable wrapper for LiveId\n#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]\npub struct LiveIdSerde(pub u64);\n\nimpl From\u003CLiveId> for LiveIdSerde {\n    fn from(id: LiveId) -> Self {\n        Self(id.0)\n    }\n}\n\nimpl From\u003CLiveIdSerde> for LiveId {\n    fn from(s: LiveIdSerde) -> Self {\n        LiveId(s.0)\n    }\n}\n```\n\n## State Propagation via Scope\n\n### Passing State Through Widget Tree\n\n```rust\nimpl AppMain for App {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {\n        \u002F\u002F Forward to MatchEvent\n        self.match_event(cx, event);\n\n        \u002F\u002F Create Scope with AppState data\n        let scope = &mut Scope::with_data(&mut self.app_state);\n\n        \u002F\u002F Pass to widget tree - all children can access AppState\n        self.ui.handle_event(cx, event, scope);\n    }\n}\n```\n\n### Accessing State in Child Widgets\n\n```rust\nimpl Widget for RoomScreen {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {\n        \u002F\u002F Access AppState from scope\n        if let Some(app_state) = scope.data.get::\u003CAppState>() {\n            if let Some(selected) = &app_state.selected_room {\n                self.update_for_room(cx, selected);\n            }\n        }\n\n        self.view.handle_event(cx, event, scope);\n    }\n}\n```\n\n### Modifying State\n\n```rust\nimpl Widget for RoomsList {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {\n        \u002F\u002F Mutable access to AppState\n        if let Some(app_state) = scope.data.get_mut::\u003CAppState>() {\n            if self.selection_changed {\n                app_state.selected_room = self.get_selected();\n            }\n        }\n    }\n}\n```\n\n## Persistence Layer\n\n### File Paths\n\n```rust\nuse std::path::{Path, PathBuf};\n\nconst LATEST_APP_STATE_FILE_NAME: &str = \"latest_app_state.json\";\nconst WINDOW_GEOM_STATE_FILE_NAME: &str = \"window_geom_state.json\";\n\n\u002F\u002F\u002F Get user-specific persistent state directory\nfn persistent_state_dir(user_id: &UserId) -> PathBuf {\n    app_data_dir()\n        .join(\"users\")\n        .join(user_id.to_string().replace(':', \"_\"))\n}\n\n\u002F\u002F\u002F Get app-wide data directory\nfn app_data_dir() -> &'static Path {\n    \u002F\u002F Platform-specific app data location\n    static APP_DATA_DIR: OnceLock\u003CPathBuf> = OnceLock::new();\n    APP_DATA_DIR.get_or_init(|| {\n        dirs::data_dir()\n            .unwrap_or_else(|| PathBuf::from(\".\"))\n            .join(\"myapp\")\n    })\n}\n```\n\n### Saving State\n\n```rust\nuse std::io::Write;\n\npub fn save_app_state(\n    app_state: AppState,\n    user_id: OwnedUserId,\n) -> anyhow::Result\u003C()> {\n    let file = std::fs::File::create(\n        persistent_state_dir(&user_id).join(LATEST_APP_STATE_FILE_NAME)\n    )?;\n    let mut writer = std::io::BufWriter::new(file);\n    serde_json::to_writer(&mut writer, &app_state)?;\n    writer.flush()?;\n    log!(\"Successfully saved app state to persistent storage.\");\n    Ok(())\n}\n\n\u002F\u002F\u002F Save window geometry state (user-agnostic)\npub fn save_window_state(window_ref: WindowRef, cx: &Cx) -> anyhow::Result\u003C()> {\n    let inner_size = window_ref.get_inner_size(cx);\n    let position = window_ref.get_position(cx);\n    let window_geom = WindowGeomState {\n        inner_size: (inner_size.x, inner_size.y),\n        position: (position.x, position.y),\n        is_fullscreen: window_ref.is_fullscreen(cx),\n    };\n    std::fs::write(\n        app_data_dir().join(WINDOW_GEOM_STATE_FILE_NAME),\n        serde_json::to_string(&window_geom)?,\n    )?;\n    Ok(())\n}\n```\n\n### Loading State\n\n```rust\n\u002F\u002F\u002F Load app state with graceful fallback\npub async fn load_app_state(user_id: &UserId) -> anyhow::Result\u003CAppState> {\n    let state_path = persistent_state_dir(user_id).join(LATEST_APP_STATE_FILE_NAME);\n\n    \u002F\u002F Read file\n    let file_bytes = match tokio::fs::read(&state_path).await {\n        Ok(fb) => fb,\n        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {\n            log!(\"No saved app state found, using default.\");\n            return Ok(AppState::default());\n        }\n        Err(e) => return Err(e.into()),\n    };\n\n    \u002F\u002F Deserialize with fallback\n    match serde_json::from_slice(&file_bytes) {\n        Ok(app_state) => {\n            log!(\"Successfully loaded app state.\");\n            Ok(app_state)\n        }\n        Err(e) => {\n            error!(\"Failed to deserialize: {e}. May be incompatible format.\");\n\n            \u002F\u002F Backup old file\n            let backup_path = state_path.with_extension(\"json.bak\");\n            if let Err(backup_err) = tokio::fs::rename(&state_path, &backup_path).await {\n                error!(\"Failed to backup old state: {}\", backup_err);\n            } else {\n                log!(\"Old state backed up to: {:?}\", backup_path);\n            }\n\n            log!(\"Using default app state.\");\n            Ok(AppState::default())\n        }\n    }\n}\n\n\u002F\u002F\u002F Load window geometry (synchronous, on UI thread)\npub fn load_window_state(window_ref: WindowRef, cx: &mut Cx) -> anyhow::Result\u003C()> {\n    let file = match std::fs::File::open(app_data_dir().join(WINDOW_GEOM_STATE_FILE_NAME)) {\n        Ok(file) => file,\n        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),\n        Err(e) => return Err(e.into()),\n    };\n\n    let window_geom: WindowGeomState = serde_json::from_reader(file)?;\n    log!(\"Restoring window geometry: {window_geom:?}\");\n\n    window_ref.configure_window(\n        cx,\n        dvec2(window_geom.inner_size.0, window_geom.inner_size.1),\n        dvec2(window_geom.position.0, window_geom.position.1),\n        window_geom.is_fullscreen,\n        \"MyApp\".to_string(),\n    );\n    Ok(())\n}\n```\n\n### Startup\u002FShutdown Integration\n\n```rust\nimpl MatchEvent for App {\n    fn handle_startup(&mut self, cx: &mut Cx) {\n        \u002F\u002F Load window geometry (sync, on UI thread)\n        if let Err(e) = persistence::load_window_state(\n            self.ui.window(ids!(main_window)), cx\n        ) {\n            error!(\"Failed to load window state: {}\", e);\n        }\n\n        \u002F\u002F Trigger async app state load\n        let user_id = get_current_user_id();\n        tokio::spawn(async move {\n            match persistence::load_app_state(&user_id).await {\n                Ok(app_state) => {\n                    Cx::post_action(AppStateAction::RestoreFromPersistence(app_state));\n                    SignalToUI::set_ui_signal();\n                }\n                Err(e) => error!(\"Failed to load app state: {}\", e),\n            }\n        });\n    }\n}\n\nimpl AppMain for App {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {\n        if let Event::Shutdown = event {\n            \u002F\u002F Save window state (sync)\n            if let Err(e) = persistence::save_window_state(\n                self.ui.window(ids!(main_window)), cx\n            ) {\n                error!(\"Failed to save window state: {e}\");\n            }\n\n            \u002F\u002F Save app state (sync)\n            if let Some(user_id) = current_user_id() {\n                if let Err(e) = persistence::save_app_state(\n                    self.app_state.clone(), user_id\n                ) {\n                    error!(\"Failed to save app state: {e}\");\n                }\n            }\n        }\n        \u002F\u002F ...\n    }\n}\n```\n\n## Thread-Local State (UI-Only)\n\n```rust\nuse std::{cell::RefCell, rc::Rc, collections::HashMap};\n\nthread_local! {\n    \u002F\u002F\u002F UI-thread-only cache\n    static UI_CACHE: Rc\u003CRefCell\u003CHashMap\u003COwnedRoomId, CachedData>>> =\n        Rc::new(RefCell::new(HashMap::new()));\n}\n\n\u002F\u002F\u002F Get cache reference (requires Cx to ensure UI thread)\npub fn get_ui_cache(_cx: &mut Cx) -> Rc\u003CRefCell\u003CHashMap\u003COwnedRoomId, CachedData>>> {\n    UI_CACHE.with(Rc::clone)\n}\n\n\u002F\u002F\u002F Clear cache (requires Cx)\npub fn clear_ui_cache(_cx: &mut Cx) {\n    UI_CACHE.with(|cache| cache.borrow_mut().clear());\n}\n```\n\n## Best Practices\n\n1. **Separate persistent vs runtime state**: Use `#[serde(skip)]` for non-persistent fields\n2. **Use Scope::with_data() for tree propagation**: Don't pass state through widget refs\n3. **Graceful deserialization fallback**: Handle format changes between versions\n4. **Backup old state files**: Preserve user data when format changes\n5. **User-specific persistent paths**: Separate state per user account\n6. **Sync window state, async app state**: Window geometry loads sync on UI thread\n7. **Thread-local for UI-only caches**: Use `thread_local!` with Cx parameter guard\n\n## Reference Files\n\n- `references\u002Fpersistence-patterns.md` - Additional persistence patterns (Robrix)\n- `references\u002Fstate-structures.md` - State structure examples (Robrix)\n- `references\u002Fmoly-state-patterns.md` - Moly-specific patterns\n  - Central Store struct containing all state\n  - Async Store initialization with `load_into_app()`\n  - App state check pattern (early return if not loaded)\n  - Submodule state managers (Search, Downloads, Chats)\n  - Provider syncing status tracking\n  - Store action forwarding to submodules\n","# Robius State Management Skill\n\nBest practices for state management and persistence in Makepad applications based on Robrix and Moly codebases.\n\n**Source codebases:**\n- **Robrix**: Matrix chat client - AppState, SelectedRoom, persistence via serde\n- **Moly**: AI chat application - Central Store pattern, async initialization, Preferences\n\n## When to Use\n\nUse this skill when:\n- Designing application state structure\n- Implementing state persistence\n- Passing state through widget tree\n- Managing UI state across sessions\n- Keywords: app state, makepad state, persistence, Scope::with_data, save state, load state\n\n## Production Patterns\n\nFor production-ready state management patterns, see the `_base\u002F` directory:\n\n| Pattern | Description |\n|---------|-------------|\n| 06-global-registry | Global widget registry with Cx::set_global |\n| 07-radio-navigation | Tab-style navigation with radio buttons |\n| 10-state-machine | Enum-based state machine widgets |\n| 11-theme-switching | Multi-theme support with apply_over |\n| 12-local-persistence | Save\u002Fload user preferences |\n\n## AppState Structure\n\n### Core State Definition\n\n```rust\nuse serde::{Serialize, Deserialize};\nuse std::collections::HashMap;\nuse matrix_sdk::ruma::OwnedRoomId;\n\n\u002F\u002F\u002F App-wide state that is stored persistently across multiple app runs\n\u002F\u002F\u002F and shared\u002Fupdated across various parts of the app.\n#[derive(Clone, Default, Debug, Serialize, Deserialize)]\npub struct AppState {\n    \u002F\u002F\u002F The currently-selected room\n    pub selected_room: Option\u003CSelectedRoom>,\n\n    \u002F\u002F\u002F Saved UI layout state for main view\n    pub saved_layout_state: SavedLayoutState,\n\n    \u002F\u002F\u002F Per-item saved states (e.g., per-space dock layouts)\n    pub saved_state_per_item: HashMap\u003COwnedRoomId, SavedLayoutState>,\n\n    \u002F\u002F\u002F Whether a user is currently logged in\n    #[serde(skip)]  \u002F\u002F Don't persist login state\n    pub logged_in: bool,\n}\n\n\u002F\u002F\u002F Represents a currently selected item\n#[derive(Clone, Debug, Serialize, Deserialize)]\npub enum SelectedRoom {\n    JoinedRoom { room_name_id: RoomNameId },\n    InvitedRoom { room_name_id: RoomNameId },\n    Space { space_name_id: RoomNameId },\n}\n\nimpl SelectedRoom {\n    pub fn room_id(&self) -> &OwnedRoomId {\n        match self {\n            Self::JoinedRoom { room_name_id } => room_name_id.room_id(),\n            Self::InvitedRoom { room_name_id } => room_name_id.room_id(),\n            Self::Space { space_name_id } => space_name_id.room_id(),\n        }\n    }\n\n    \u002F\u002F\u002F Upgrade from invited to joined state\n    pub fn upgrade_invite_to_joined(&mut self, room_id: &RoomId) -> bool {\n        match self {\n            Self::InvitedRoom { room_name_id } if room_name_id.room_id() == room_id => {\n                let name = room_name_id.clone();\n                *self = Self::JoinedRoom { room_name_id: name };\n                true\n            }\n            _ => false,\n        }\n    }\n}\n\n\u002F\u002F Equality based on room_id only\nimpl PartialEq for SelectedRoom {\n    fn eq(&self, other: &Self) -> bool {\n        self.room_id() == other.room_id()\n    }\n}\nimpl Eq for SelectedRoom {}\n```\n\n### Layout\u002FDock State Persistence\n\n```rust\n\u002F\u002F\u002F A snapshot of UI layout state for restoration\n#[derive(Clone, Default, Debug, Serialize, Deserialize)]\npub struct SavedLayoutState {\n    \u002F\u002F\u002F All items contained in the layout, keyed by ID\n    pub layout_items: HashMap\u003CLiveIdSerde, LayoutItemSerde>,\n\n    \u002F\u002F\u002F Items currently open, keyed by ID\n    pub open_items: HashMap\u003CLiveIdSerde, SelectedRoom>,\n\n    \u002F\u002F\u002F Order items were opened (chronological)\n    pub item_order: Vec\u003CSelectedRoom>,\n\n    \u002F\u002F\u002F Currently selected item when state was saved\n    pub selected_item: Option\u003CSelectedRoom>,\n}\n\n\u002F\u002F\u002F Serializable wrapper for LiveId\n#[derive(Clone, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]\npub struct LiveIdSerde(pub u64);\n\nimpl From\u003CLiveId> for LiveIdSerde {\n    fn from(id: LiveId) -> Self {\n        Self(id.0)\n    }\n}\n\nimpl From\u003CLiveIdSerde> for LiveId {\n    fn from(s: LiveIdSerde) -> Self {\n        LiveId(s.0)\n    }\n}\n```\n\n## State Propagation via Scope\n\n### Passing State Through Widget Tree\n\n```rust\nimpl AppMain for App {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {\n        \u002F\u002F Forward to MatchEvent\n        self.match_event(cx, event);\n\n        \u002F\u002F Create Scope with AppState data\n        let scope = &mut Scope::with_data(&mut self.app_state);\n\n        \u002F\u002F Pass to widget tree - all children can access AppState\n        self.ui.handle_event(cx, event, scope);\n    }\n}\n```\n\n### Accessing State in Child Widgets\n\n```rust\nimpl Widget for RoomScreen {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {\n        \u002F\u002F Access AppState from scope\n        if let Some(app_state) = scope.data.get::\u003CAppState>() {\n            if let Some(selected) = &app_state.selected_room {\n                self.update_for_room(cx, selected);\n            }\n        }\n\n        self.view.handle_event(cx, event, scope);\n    }\n}\n```\n\n### Modifying State\n\n```rust\nimpl Widget for RoomsList {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {\n        \u002F\u002F Mutable access to AppState\n        if let Some(app_state) = scope.data.get_mut::\u003CAppState>() {\n            if self.selection_changed {\n                app_state.selected_room = self.get_selected();\n            }\n        }\n    }\n}\n```\n\n## Persistence Layer\n\n### File Paths\n\n```rust\nuse std::path::{Path, PathBuf};\n\nconst LATEST_APP_STATE_FILE_NAME: &str = \"latest_app_state.json\";\nconst WINDOW_GEOM_STATE_FILE_NAME: &str = \"window_geom_state.json\";\n\n\u002F\u002F\u002F Get user-specific persistent state directory\nfn persistent_state_dir(user_id: &UserId) -> PathBuf {\n    app_data_dir()\n        .join(\"users\")\n        .join(user_id.to_string().replace(':', \"_\"))\n}\n\n\u002F\u002F\u002F Get app-wide data directory\nfn app_data_dir() -> &'static Path {\n    \u002F\u002F Platform-specific app data location\n    static APP_DATA_DIR: OnceLock\u003CPathBuf> = OnceLock::new();\n    APP_DATA_DIR.get_or_init(|| {\n        dirs::data_dir()\n            .unwrap_or_else(|| PathBuf::from(\".\"))\n            .join(\"myapp\")\n    })\n}\n```\n\n### Saving State\n\n```rust\nuse std::io::Write;\n\npub fn save_app_state(\n    app_state: AppState,\n    user_id: OwnedUserId,\n) -> anyhow::Result\u003C()> {\n    let file = std::fs::File::create(\n        persistent_state_dir(&user_id).join(LATEST_APP_STATE_FILE_NAME)\n    )?;\n    let mut writer = std::io::BufWriter::new(file);\n    serde_json::to_writer(&mut writer, &app_state)?;\n    writer.flush()?;\n    log!(\"Successfully saved app state to persistent storage.\");\n    Ok(())\n}\n\n\u002F\u002F\u002F Save window geometry state (user-agnostic)\npub fn save_window_state(window_ref: WindowRef, cx: &Cx) -> anyhow::Result\u003C()> {\n    let inner_size = window_ref.get_inner_size(cx);\n    let position = window_ref.get_position(cx);\n    let window_geom = WindowGeomState {\n        inner_size: (inner_size.x, inner_size.y),\n        position: (position.x, position.y),\n        is_fullscreen: window_ref.is_fullscreen(cx),\n    };\n    std::fs::write(\n        app_data_dir().join(WINDOW_GEOM_STATE_FILE_NAME),\n        serde_json::to_string(&window_geom)?,\n    )?;\n    Ok(())\n}\n```\n\n### Loading State\n\n```rust\n\u002F\u002F\u002F Load app state with graceful fallback\npub async fn load_app_state(user_id: &UserId) -> anyhow::Result\u003CAppState> {\n    let state_path = persistent_state_dir(user_id).join(LATEST_APP_STATE_FILE_NAME);\n\n    \u002F\u002F Read file\n    let file_bytes = match tokio::fs::read(&state_path).await {\n        Ok(fb) => fb,\n        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {\n            log!(\"No saved app state found, using default.\");\n            return Ok(AppState::default());\n        }\n        Err(e) => return Err(e.into()),\n    };\n\n    \u002F\u002F Deserialize with fallback\n    match serde_json::from_slice(&file_bytes) {\n        Ok(app_state) => {\n            log!(\"Successfully loaded app state.\");\n            Ok(app_state)\n        }\n        Err(e) => {\n            error!(\"Failed to deserialize: {e}. May be incompatible format.\");\n\n            \u002F\u002F Backup old file\n            let backup_path = state_path.with_extension(\"json.bak\");\n            if let Err(backup_err) = tokio::fs::rename(&state_path, &backup_path).await {\n                error!(\"Failed to backup old state: {}\", backup_err);\n            } else {\n                log!(\"Old state backed up to: {:?}\", backup_path);\n            }\n\n            log!(\"Using default app state.\");\n            Ok(AppState::default())\n        }\n    }\n}\n\n\u002F\u002F\u002F Load window geometry (synchronous, on UI thread)\npub fn load_window_state(window_ref: WindowRef, cx: &mut Cx) -> anyhow::Result\u003C()> {\n    let file = match std::fs::File::open(app_data_dir().join(WINDOW_GEOM_STATE_FILE_NAME)) {\n        Ok(file) => file,\n        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),\n        Err(e) => return Err(e.into()),\n    };\n\n    let window_geom: WindowGeomState = serde_json::from_reader(file)?;\n    log!(\"Restoring window geometry: {window_geom:?}\");\n\n    window_ref.configure_window(\n        cx,\n        dvec2(window_geom.inner_size.0, window_geom.inner_size.1),\n        dvec2(window_geom.position.0, window_geom.position.1),\n        window_geom.is_fullscreen,\n        \"MyApp\".to_string(),\n    );\n    Ok(())\n}\n```\n\n### Startup\u002FShutdown Integration\n\n```rust\nimpl MatchEvent for App {\n    fn handle_startup(&mut self, cx: &mut Cx) {\n        \u002F\u002F Load window geometry (sync, on UI thread)\n        if let Err(e) = persistence::load_window_state(\n            self.ui.window(ids!(main_window)), cx\n        ) {\n            error!(\"Failed to load window state: {}\", e);\n        }\n\n        \u002F\u002F Trigger async app state load\n        let user_id = get_current_user_id();\n        tokio::spawn(async move {\n            match persistence::load_app_state(&user_id).await {\n                Ok(app_state) => {\n                    Cx::post_action(AppStateAction::RestoreFromPersistence(app_state));\n                    SignalToUI::set_ui_signal();\n                }\n                Err(e) => error!(\"Failed to load app state: {}\", e),\n            }\n        });\n    }\n}\n\nimpl AppMain for App {\n    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {\n        if let Event::Shutdown = event {\n            \u002F\u002F Save window state (sync)\n            if let Err(e) = persistence::save_window_state(\n                self.ui.window(ids!(main_window)), cx\n            ) {\n                error!(\"Failed to save window state: {e}\");\n            }\n\n            \u002F\u002F Save app state (sync)\n            if let Some(user_id) = current_user_id() {\n                if let Err(e) = persistence::save_app_state(\n                    self.app_state.clone(), user_id\n                ) {\n                    error!(\"Failed to save app state: {e}\");\n                }\n            }\n        }\n        \u002F\u002F ...\n    }\n}\n```\n\n## Thread-Local State (UI-Only)\n\n```rust\nuse std::{cell::RefCell, rc::Rc, collections::HashMap};\n\nthread_local! {\n    \u002F\u002F\u002F UI-thread-only cache\n    static UI_CACHE: Rc\u003CRefCell\u003CHashMap\u003COwnedRoomId, CachedData>>> =\n        Rc::new(RefCell::new(HashMap::new()));\n}\n\n\u002F\u002F\u002F Get cache reference (requires Cx to ensure UI thread)\npub fn get_ui_cache(_cx: &mut Cx) -> Rc\u003CRefCell\u003CHashMap\u003COwnedRoomId, CachedData>>> {\n    UI_CACHE.with(Rc::clone)\n}\n\n\u002F\u002F\u002F Clear cache (requires Cx)\npub fn clear_ui_cache(_cx: &mut Cx) {\n    UI_CACHE.with(|cache| cache.borrow_mut().clear());\n}\n```\n\n## Best Practices\n\n1. **Separate persistent vs runtime state**: Use `#[serde(skip)]` for non-persistent fields\n2. **Use Scope::with_data() for tree propagation**: Don't pass state through widget refs\n3. **Graceful deserialization fallback**: Handle format changes between versions\n4. **Backup old state files**: Preserve user data when format changes\n5. **User-specific persistent paths**: Separate state per user account\n6. **Sync window state, async app state**: Window geometry loads sync on UI thread\n7. **Thread-local for UI-only caches**: Use `thread_local!` with Cx parameter guard\n\n## Reference Files\n\n- `references\u002Fpersistence-patterns.md` - Additional persistence patterns (Robrix)\n- `references\u002Fstate-structures.md` - State structure examples (Robrix)\n- `references\u002Fmoly-state-patterns.md` - Moly-specific patterns\n  - Central Store struct containing all state\n  - Async Store initialization with `load_into_app()`\n  - App state check pattern (early return if not loaded)\n  - Submodule state managers (Search, Downloads, Chats)\n  - Provider syncing status tracking\n  - Store action forwarding to submodules\n",{"name":5,"risk":35,"source":27,"description":36},"unknown","CRITICAL: Use for Robius state management patterns. Triggers on:\nAppState, persistence, theme switch, 状态管理,\nScope::with_data, save state, load state, serde,\n状态持久化, 主题切换\n","main","a50a3aa6b39fb0e1b7db88bef1bfc9c93606e126","modules\u002Fagent_skills\u002Fawesome-skills\u002Fplaybooks\u002Frobius-state-management",0,"https:\u002F\u002Fgithub.com\u002FSam-06060\u002Fjarvis-assistant","https:\u002F\u002Fgithub.com\u002FSam-06060","Mobile Application Developer | Kotlin | Flutter, KMP, Swfit, Android,Swift UI | Building at Saar It Resources | Creator of Jarvis (AI Assistant)","2026-06-23 05:13:38",{"source":8,"skillName":5,"commands":46,"prompt":56,"zipAvailable":57},[47,50,53],{"tool":48,"command":49},"npx","npx skills add Sam-06060\u002Fjarvis-assistant --skill robius-state-management",{"tool":51,"command":52},"bunx","bunx skills add Sam-06060\u002Fjarvis-assistant --skill robius-state-management",{"tool":54,"command":55},"pnpm","pnpm dlx skills add Sam-06060\u002Fjarvis-assistant --skill robius-state-management","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FSam-06060\u002Fjarvis-assistant\u002Fblob\u002Fmain\u002Fmodules\u002Fagent_skills\u002Fawesome-skills\u002Fplaybooks\u002Frobius-state-management\u002FSKILL.md",true,[],[60,73,88,114,133,160],{"skillNo":61,"name":62,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":63,"categories":64,"keywords":70,"publishedAt":30},"01M6GDRVS6FXX3ESH0C9DW01C0","test_skill","https:\u002F\u002Fgithub.com\u002FSam-06060\u002Fjarvis-assistant\u002Fblob\u002Fmain\u002Fscratch\u002Ftest_skill\u002FSKILL.md",[65],{"categoryNo":66,"categoryName":67,"taxonomyType":68,"taxonomyTypeDesc":69},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",[71,72],"Test Skill","test",{"skillNo":74,"name":75,"nameZh":6,"description":76,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":77,"categories":78,"keywords":79,"publishedAt":30},"01M6GDRQ40HX22MA6XWC31RHHK","zustand-store-ts","Create Zustand stores following established patterns with proper TypeScript types and middleware.","https:\u002F\u002Fgithub.com\u002FSam-06060\u002Fjarvis-assistant\u002Fblob\u002Fmain\u002Fmodules\u002Fagent_skills\u002Fawesome-skills\u002Fplaybooks\u002Fzustand-store-ts\u002FSKILL.md",[],[80,81,82,83,84,85,29,27,86,87],"zustand store ts","zustand","store","with","types","middleware","create","typescript",{"skillNo":89,"name":90,"nameZh":6,"description":91,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":92,"categories":93,"keywords":97,"publishedAt":30},"01M6GDRKEA0WE670SPFWX5GKCN","zoom-automation","Automate Zoom meeting creation, management, recordings, webinars, and participant tracking via Rube MCP (Composio). Always search tools first for current schemas.","https:\u002F\u002Fgithub.com\u002FSam-06060\u002Fjarvis-assistant\u002Fblob\u002Fmain\u002Fmodules\u002Fagent_skills\u002Fawesome-skills\u002Fplaybooks\u002Fzoom-automation\u002FSKILL.md",[94],{"categoryNo":95,"categoryName":96,"taxonomyType":68,"taxonomyTypeDesc":69},"01JBSEEDCAT000000000000023","Skill与智能体开发",[98,99,100,101,102,103,104,105,106,107,108,109,110,111,27,112,21,113],"automation","zoom automation","zoom","first","Tools","search","MCP","via","meeting","automate","critical","creation","webinars","composio","recordings","tracking",{"skillNo":115,"name":116,"nameZh":6,"description":117,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":118,"categories":119,"keywords":121,"publishedAt":30},"01M6GDRFS1452PC71JQ38S24HR","zoho-crm-automation","Automate Zoho CRM tasks via Rube MCP (Composio): create\u002Fupdate records, search contacts, manage leads, and convert leads. Always search tools first for current schemas.","https:\u002F\u002Fgithub.com\u002FSam-06060\u002Fjarvis-assistant\u002Fblob\u002Fmain\u002Fmodules\u002Fagent_skills\u002Fawesome-skills\u002Fplaybooks\u002Fzoho-crm-automation\u002FSKILL.md",[120],{"categoryNo":95,"categoryName":96,"taxonomyType":68,"taxonomyTypeDesc":69},[122,98,123,124,125,126,127,27,103,128,111,129,108,107,102,130,86,131,104,132],"crm automation","zoho crm","crm","zoho crm automation","zoho","leads","contacts","records","update","tasks","manage",{"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":142,"publishedAt":30},"01M6GDRCKQBAVTGHX5XBH26KPZ","zod-validation-expert","Expert in Zod — TypeScript-first schema validation. Covers parsing, custom errors, refinements, type inference, and integration with React Hook Form, Next.js, and tRPC.","https:\u002F\u002Fgithub.com\u002FSam-06060\u002Fjarvis-assistant\u002Fblob\u002Fmain\u002Fmodules\u002Fagent_skills\u002Fawesome-skills\u002Fplaybooks\u002Fzod-validation-expert\u002FSKILL.md",[139],{"categoryNo":140,"categoryName":141,"taxonomyType":68,"taxonomyTypeDesc":69},"01JBSEEDCAT000000000000009","前端开发",[143,144,145,146,147,87,148,149,150,151,152,153,83,154,155,156,157,27,158,159],"expert","zod","zod validation expert","validation","react","safe","parsing","errors","schema validation","react hook form","hook","trpc","form","type","next","schema","Next.js",{"skillNo":161,"name":162,"nameZh":6,"description":163,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":164,"categories":165,"keywords":167,"publishedAt":30},"01M6GDR658B08K4MY0RWD4A6EH","zeroize-audit","Detects missing zeroization of sensitive data in source code and identifies zeroization removed by compiler optimizations, with assembly-level analysis, and control-flow verification. Use for auditing C\u002FC++\u002FRust code handling secrets, keys, passwords, or other sensitive data.","https:\u002F\u002Fgithub.com\u002FSam-06060\u002Fjarvis-assistant\u002Fblob\u002Fmain\u002Fmodules\u002Fagent_skills\u002Fawesome-skills\u002Fplaybooks\u002Fzeroize-audit\u002FSKILL.md",[166],{"categoryNo":95,"categoryName":96,"taxonomyType":68,"taxonomyTypeDesc":69},[168,169,170,171,172,173,174,175,27,176,177,178,179,180,24,181,182,183,184,104],"zeroize audit","audit","write","passwords","get","project","c++","activate","assembly","read","Bash","secrets","analysis","flow","verification","code","rust"]