mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(about_app): add a runtime capability catalog for app discovery (#267)
* feat(about_app): introduce user-facing capability catalog - Added a new module for the capability catalog, providing a single source of truth for user-facing features in the OpenHuman app. - Implemented functions for listing, looking up, and searching capabilities, enhancing user interaction with the app's features. - Created a structured schema for capabilities, including categories and statuses, to improve organization and accessibility. - Added comprehensive end-to-end tests to validate the functionality of the new capability catalog, ensuring robust performance and reliability. This update significantly enhances the app's ability to expose its features to users, improving overall usability and experience. * feat(about_app): update capability catalog with new features and improved instructions - Revised existing capability instructions for clarity and accuracy, enhancing user guidance on accessing features. - Introduced several new capabilities related to skills and authentication, including connections to Google, Notion, and Web3 wallets, expanding the app's functionality. - Added new settings management capabilities, allowing users to manage desktop services and clear app data, improving user control over the application. These updates significantly enhance the capability catalog, providing users with more options and clearer instructions for utilizing the app's features.
This commit is contained in:
@@ -335,6 +335,8 @@ Follow this order so behavior is **specified**, **proven in Rust**, **proven ove
|
||||
5. **App unit tests** — Cover components, hooks, and clients with **Vitest** (`yarn test` / `yarn test:unit` in `app/`).
|
||||
6. **App E2E** — Add **desktop E2E** specs where the feature is user-visible (`yarn test:e2e*`, isolated workspace — see [Testing Guide (Unit + E2E)](#testing-guide-unit--e2e)) so the full stack (UI → Tauri → sidecar) behaves as intended.
|
||||
|
||||
**Capability catalog** — When a change adds, removes, renames, relocates, or materially changes a user-facing feature, update **`src/openhuman/about_app/`** in the same work so the runtime capability catalog remains the source of truth for what the app can do.
|
||||
|
||||
**Debug logging (throughout)** — Add **lots of development-oriented logging** as you build, not as an afterthought. In **Rust**, use `log` / `tracing` at **`debug`** or **`trace`** on RPC entry and exit, error paths, state transitions, and any branch that is hard to infer from tests alone. In **`app/`**, follow existing patterns (e.g. the **`debug`** npm package with a **namespace** per area) plus **dev-only** detail where useful. Prefer **grep-friendly prefixes** (`[feature]`, domain name, or JSON-RPC method) so terminal output from **sidecar**, **Tauri**, and **WebView** can be correlated during `yarn dev` / `tauri dev`. **Never** log secrets, raw JWTs, API keys, or full PII—redact or omit.
|
||||
|
||||
**Planning rule:** When scoping a feature, define the **E2E scenarios (core RPC + app)** up front. Those scenarios should **cover the full intended scope**—happy paths, failure modes, auth or policy gates, and regressions you care about. If a scenario is not testable end-to-end, the spec is incomplete or the cut is too large; split or add harness support first.
|
||||
|
||||
@@ -38,6 +38,7 @@ fn registry() -> &'static [RegisteredController] {
|
||||
|
||||
fn build_registered_controllers() -> Vec<RegisteredController> {
|
||||
let mut controllers = Vec::new();
|
||||
controllers.extend(crate::openhuman::about_app::all_about_app_registered_controllers());
|
||||
controllers.extend(crate::openhuman::cron::all_cron_registered_controllers());
|
||||
controllers.extend(crate::openhuman::agent::all_agent_registered_controllers());
|
||||
controllers.extend(crate::openhuman::health::all_health_registered_controllers());
|
||||
@@ -71,6 +72,7 @@ fn build_registered_controllers() -> Vec<RegisteredController> {
|
||||
|
||||
fn build_declared_controller_schemas() -> Vec<ControllerSchema> {
|
||||
let mut schemas = Vec::new();
|
||||
schemas.extend(crate::openhuman::about_app::all_about_app_controller_schemas());
|
||||
schemas.extend(crate::openhuman::cron::all_cron_controller_schemas());
|
||||
schemas.extend(crate::openhuman::agent::all_agent_controller_schemas());
|
||||
schemas.extend(crate::openhuman::health::all_health_controller_schemas());
|
||||
@@ -115,6 +117,7 @@ pub fn rpc_method_name(schema: &ControllerSchema) -> String {
|
||||
|
||||
pub fn namespace_description(namespace: &str) -> Option<&'static str> {
|
||||
match namespace {
|
||||
"about_app" => Some("Catalog the app's user-facing capabilities and where to find them."),
|
||||
"auth" => Some("Manage app session and provider credentials."),
|
||||
"autocomplete" => Some("Inline autocomplete engine controls and style settings."),
|
||||
"channels" => Some("Channel definitions, connections, and lifecycle management."),
|
||||
|
||||
@@ -0,0 +1,774 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use super::types::{Capability, CapabilityCategory, CapabilityStatus};
|
||||
|
||||
const CAPABILITIES: &[Capability] = &[
|
||||
Capability {
|
||||
id: "conversation.create",
|
||||
name: "Create Conversations",
|
||||
domain: "conversation",
|
||||
category: CapabilityCategory::Conversation,
|
||||
description: "Start a new conversation thread with the assistant.",
|
||||
how_to: "Conversations",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "conversation.send_text",
|
||||
name: "Send Text Messages",
|
||||
domain: "conversation",
|
||||
category: CapabilityCategory::Conversation,
|
||||
description: "Send typed messages to the assistant in a conversation.",
|
||||
how_to: "Conversations > Message composer",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "conversation.send_voice",
|
||||
name: "Send Voice Messages",
|
||||
domain: "conversation",
|
||||
category: CapabilityCategory::Conversation,
|
||||
description: "Record or attach voice input and send it as a message.",
|
||||
how_to: "Conversations > Voice input",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "conversation.inline_autocomplete",
|
||||
name: "Inline Autocomplete",
|
||||
domain: "conversation",
|
||||
category: CapabilityCategory::Conversation,
|
||||
description: "Show predictive inline text suggestions while you type.",
|
||||
how_to: "Settings > Inline Autocomplete",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "conversation.copy_messages",
|
||||
name: "Copy Messages",
|
||||
domain: "conversation",
|
||||
category: CapabilityCategory::Conversation,
|
||||
description: "Copy individual assistant or user messages for reuse elsewhere.",
|
||||
how_to: "Conversations > Message actions",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "conversation.delete_conversations",
|
||||
name: "Delete Conversations",
|
||||
domain: "conversation",
|
||||
category: CapabilityCategory::Conversation,
|
||||
description: "Remove existing conversation threads from the app.",
|
||||
how_to: "Conversations > Thread actions",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "conversation.suggested_questions",
|
||||
name: "Suggested Questions",
|
||||
domain: "conversation",
|
||||
category: CapabilityCategory::Conversation,
|
||||
description: "Offer prompt suggestions to help continue a conversation.",
|
||||
how_to: "Home or Conversations > Suggested prompts",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "conversation.tool_execution_timeline",
|
||||
name: "Tool Execution Timeline",
|
||||
domain: "conversation",
|
||||
category: CapabilityCategory::Conversation,
|
||||
description: "Show the sequence of tool calls and actions used to answer a request.",
|
||||
how_to: "Conversations > Tool timeline",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "intelligence.analyze_actionable_items",
|
||||
name: "Analyze Actionable Items",
|
||||
domain: "intelligence",
|
||||
category: CapabilityCategory::Intelligence,
|
||||
description: "Extract and summarize actionable items from your activity and conversations.",
|
||||
how_to: "Intelligence",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "intelligence.filter_actionable_items",
|
||||
name: "Filter Actionable Items",
|
||||
domain: "intelligence",
|
||||
category: CapabilityCategory::Intelligence,
|
||||
description: "Search and filter actionable items to focus on what matters now.",
|
||||
how_to: "Intelligence > Filters and search",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "intelligence.mark_actionable_item_complete",
|
||||
name: "Mark Items Complete",
|
||||
domain: "intelligence",
|
||||
category: CapabilityCategory::Intelligence,
|
||||
description: "Mark an actionable item as completed.",
|
||||
how_to: "Intelligence > Item actions",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "intelligence.dismiss_actionable_item",
|
||||
name: "Dismiss Items",
|
||||
domain: "intelligence",
|
||||
category: CapabilityCategory::Intelligence,
|
||||
description: "Dismiss irrelevant or already handled actionable items.",
|
||||
how_to: "Intelligence > Item actions",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "intelligence.snooze_actionable_item",
|
||||
name: "Snooze Items",
|
||||
domain: "intelligence",
|
||||
category: CapabilityCategory::Intelligence,
|
||||
description: "Temporarily hide an actionable item until later.",
|
||||
how_to: "Intelligence > Item actions",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "intelligence.undo_action",
|
||||
name: "Undo Item Actions",
|
||||
domain: "intelligence",
|
||||
category: CapabilityCategory::Intelligence,
|
||||
description: "Undo a recent complete, dismiss, or snooze action.",
|
||||
how_to: "Intelligence > Undo snackbar or item history",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "intelligence.memory_workspace",
|
||||
name: "Memory Workspace",
|
||||
domain: "intelligence",
|
||||
category: CapabilityCategory::Intelligence,
|
||||
description: "Inspect or debug the app's memory workspace and stored knowledge.",
|
||||
how_to: "Settings > Memory Debug",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "skills.discover",
|
||||
name: "Discover Skills",
|
||||
domain: "skills",
|
||||
category: CapabilityCategory::Skills,
|
||||
description: "Browse available skills that can extend the app.",
|
||||
how_to: "Skills",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "skills.install",
|
||||
name: "Install Skills",
|
||||
domain: "skills",
|
||||
category: CapabilityCategory::Skills,
|
||||
description: "Install a skill into the local workspace.",
|
||||
how_to: "Skills > Install",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "skills.configure",
|
||||
name: "Configure Skills",
|
||||
domain: "skills",
|
||||
category: CapabilityCategory::Skills,
|
||||
description: "Open skill setup and update skill-specific configuration.",
|
||||
how_to: "Skills > Setup or Settings > Connections",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "skills.connection_status",
|
||||
name: "Monitor Skill Connection Status",
|
||||
domain: "skills",
|
||||
category: CapabilityCategory::Skills,
|
||||
description: "See whether a skill-backed integration is connected, offline, or needs setup.",
|
||||
how_to: "Skills or Settings > Connections",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "skills.sync_manual",
|
||||
name: "Manually Sync Skill Data",
|
||||
domain: "skills",
|
||||
category: CapabilityCategory::Skills,
|
||||
description: "Trigger a manual data sync for a skill integration.",
|
||||
how_to: "Skills > Skill card > Sync",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "skills.toggle_enabled",
|
||||
name: "Enable or Disable Skills",
|
||||
domain: "skills",
|
||||
category: CapabilityCategory::Skills,
|
||||
description: "Turn individual skills on or off without uninstalling them.",
|
||||
how_to: "Settings > Developer Options > Skills",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "skills.open_connections_hub",
|
||||
name: "Open Connections Hub",
|
||||
domain: "skills",
|
||||
category: CapabilityCategory::Skills,
|
||||
description: "Browse the dedicated connections hub for external skill-backed integrations.",
|
||||
how_to: "Settings > Connections",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "skills.connect_google",
|
||||
name: "Connect Google",
|
||||
domain: "skills",
|
||||
category: CapabilityCategory::Skills,
|
||||
description: "Connect Google services for email, contacts, and calendar workflows.",
|
||||
how_to: "Settings > Connections",
|
||||
status: CapabilityStatus::ComingSoon,
|
||||
},
|
||||
Capability {
|
||||
id: "skills.connect_notion",
|
||||
name: "Connect Notion",
|
||||
domain: "skills",
|
||||
category: CapabilityCategory::Skills,
|
||||
description: "Connect Notion for workspace sync and productivity workflows.",
|
||||
how_to: "Settings > Connections",
|
||||
status: CapabilityStatus::ComingSoon,
|
||||
},
|
||||
Capability {
|
||||
id: "skills.connect_web3_wallet",
|
||||
name: "Connect Web3 Wallet",
|
||||
domain: "skills",
|
||||
category: CapabilityCategory::Skills,
|
||||
description: "Connect a wallet for crypto workflows and onchain actions.",
|
||||
how_to: "Settings > Connections",
|
||||
status: CapabilityStatus::ComingSoon,
|
||||
},
|
||||
Capability {
|
||||
id: "skills.connect_crypto_exchange",
|
||||
name: "Connect Crypto Exchange",
|
||||
domain: "skills",
|
||||
category: CapabilityCategory::Skills,
|
||||
description: "Connect supported exchanges for trading and portfolio workflows.",
|
||||
how_to: "Settings > Connections",
|
||||
status: CapabilityStatus::ComingSoon,
|
||||
},
|
||||
Capability {
|
||||
id: "skills.browser_access_policy",
|
||||
name: "Configure Browser Access Policy",
|
||||
domain: "skills",
|
||||
category: CapabilityCategory::Skills,
|
||||
description: "Control whether browser-based tools can visit only the allowlist or any public domain.",
|
||||
how_to: "Settings > Developer Options > Skills > Browser Access",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "local_ai.download_model",
|
||||
name: "Download Local Models",
|
||||
domain: "local_ai",
|
||||
category: CapabilityCategory::LocalAI,
|
||||
description: "Download and bootstrap local AI runtimes and model bundles.",
|
||||
how_to: "Settings > Local AI Model",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "local_ai.manage_model_assets",
|
||||
name: "Manage Model Assets",
|
||||
domain: "local_ai",
|
||||
category: CapabilityCategory::LocalAI,
|
||||
description: "Inspect asset status and download specific chat, vision, embedding, STT, or TTS assets.",
|
||||
how_to: "Settings > Local AI Model > Advanced > Capability Assets",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "local_ai.embed_text",
|
||||
name: "Generate Text Embeddings",
|
||||
domain: "local_ai",
|
||||
category: CapabilityCategory::LocalAI,
|
||||
description: "Create local vector embeddings for text input.",
|
||||
how_to: "Settings > Local AI Model > Advanced > Test Embeddings",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "local_ai.speech_to_text",
|
||||
name: "Speech Recognition",
|
||||
domain: "local_ai",
|
||||
category: CapabilityCategory::LocalAI,
|
||||
description: "Transcribe audio into text using local speech recognition.",
|
||||
how_to: "Settings > Local AI Model > Advanced > Test Voice Input",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "local_ai.text_to_speech",
|
||||
name: "Text to Speech",
|
||||
domain: "local_ai",
|
||||
category: CapabilityCategory::LocalAI,
|
||||
description: "Synthesize speech from text using local voice models.",
|
||||
how_to: "Settings > Local AI Model > Advanced > Test Voice Output",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "local_ai.vision_processing",
|
||||
name: "Vision Processing",
|
||||
domain: "local_ai",
|
||||
category: CapabilityCategory::LocalAI,
|
||||
description: "Run vision prompts against images using a local multimodal model.",
|
||||
how_to: "Settings > Local AI Model > Advanced > Test Vision Prompt",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "local_ai.direct_prompting",
|
||||
name: "Direct Model Prompting",
|
||||
domain: "local_ai",
|
||||
category: CapabilityCategory::LocalAI,
|
||||
description: "Send a direct prompt to the local model without using the cloud API.",
|
||||
how_to: "Settings > Local AI Model > Advanced > Test Custom Prompt",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "team.create",
|
||||
name: "Create Teams",
|
||||
domain: "team",
|
||||
category: CapabilityCategory::Team,
|
||||
description: "Create a team and start collaborating with shared billing and members.",
|
||||
how_to: "Settings > Team",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "team.join_via_invite_code",
|
||||
name: "Join Teams via Invite Code",
|
||||
domain: "team",
|
||||
category: CapabilityCategory::Team,
|
||||
description: "Join an existing team using an invite code.",
|
||||
how_to: "Invites > Redeem an Invite Code",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "team.switch_active_team",
|
||||
name: "Switch Active Team",
|
||||
domain: "team",
|
||||
category: CapabilityCategory::Team,
|
||||
description: "Switch which team is currently active in the app.",
|
||||
how_to: "Settings > Team",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "team.leave",
|
||||
name: "Leave Teams",
|
||||
domain: "team",
|
||||
category: CapabilityCategory::Team,
|
||||
description: "Leave a team that you no longer want to participate in.",
|
||||
how_to: "Settings > Team",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "team.manage_members",
|
||||
name: "Manage Team Members",
|
||||
domain: "team",
|
||||
category: CapabilityCategory::Team,
|
||||
description: "Review members and change team roles when you have permission.",
|
||||
how_to: "Settings > Team > Manage team > Members",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "team.generate_invite_codes",
|
||||
name: "Generate Invite Codes",
|
||||
domain: "team",
|
||||
category: CapabilityCategory::Team,
|
||||
description: "Create invite codes to bring new members into a team.",
|
||||
how_to: "Settings > Team > Manage team > Invites",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "team.track_invite_usage",
|
||||
name: "Track Invite Usage",
|
||||
domain: "team",
|
||||
category: CapabilityCategory::Team,
|
||||
description: "View invite usage counts, limits, and revoke team invites.",
|
||||
how_to: "Settings > Team > Manage team > Invites",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "auth.login_oauth",
|
||||
name: "Login via OAuth",
|
||||
domain: "auth",
|
||||
category: CapabilityCategory::Auth,
|
||||
description: "Sign in with the app's supported provider-based authentication flow.",
|
||||
how_to: "Welcome",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "auth.onboarding_setup",
|
||||
name: "Onboarding Setup",
|
||||
domain: "auth",
|
||||
category: CapabilityCategory::Auth,
|
||||
description: "Walk through onboarding to configure initial permissions and preferences.",
|
||||
how_to: "Onboarding",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "auth.configure_tool_access",
|
||||
name: "Configure Tool Access",
|
||||
domain: "auth",
|
||||
category: CapabilityCategory::Auth,
|
||||
description: "Choose which built-in tools OpenHuman can use on your behalf during setup.",
|
||||
how_to: "Onboarding > Enable Tools",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "auth.backup_recovery_phrase",
|
||||
name: "Back Up Recovery Phrase",
|
||||
domain: "auth",
|
||||
category: CapabilityCategory::Auth,
|
||||
description: "Generate and save a recovery phrase used to secure and restore encrypted app data.",
|
||||
how_to: "Onboarding > Recovery Phrase",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "auth.import_recovery_phrase",
|
||||
name: "Import Recovery Phrase",
|
||||
domain: "auth",
|
||||
category: CapabilityCategory::Auth,
|
||||
description: "Import an existing recovery phrase to restore encrypted app data.",
|
||||
how_to: "Onboarding > Recovery Phrase > I already have a recovery phrase",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "auth.logout",
|
||||
name: "Logout",
|
||||
domain: "auth",
|
||||
category: CapabilityCategory::Auth,
|
||||
description: "Sign out of the current session.",
|
||||
how_to: "Settings > Log out",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "screen_intelligence.toggle_monitoring",
|
||||
name: "Enable or Disable Screen Monitoring",
|
||||
domain: "screen_intelligence",
|
||||
category: CapabilityCategory::ScreenIntelligence,
|
||||
description: "Turn desktop screen intelligence capture on or off.",
|
||||
how_to: "Settings > Screen Intelligence",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "screen_intelligence.manage_accessibility_permissions",
|
||||
name: "Manage Accessibility Permissions",
|
||||
domain: "screen_intelligence",
|
||||
category: CapabilityCategory::ScreenIntelligence,
|
||||
description: "Review and grant the accessibility permissions required for desktop assistance.",
|
||||
how_to: "Onboarding > Screen permissions or Settings > Accessibility Automation",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "screen_intelligence.review_vision_data",
|
||||
name: "Review Vision Data",
|
||||
domain: "screen_intelligence",
|
||||
category: CapabilityCategory::ScreenIntelligence,
|
||||
description: "Inspect the captured screen intelligence and related vision summaries.",
|
||||
how_to: "Settings > Screen Intelligence",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "screen_intelligence.configure_capture_fps",
|
||||
name: "Configure Capture FPS",
|
||||
domain: "screen_intelligence",
|
||||
category: CapabilityCategory::ScreenIntelligence,
|
||||
description: "Tune the screen capture frame rate used by screen intelligence.",
|
||||
how_to: "Settings > Screen Intelligence",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "screen_intelligence.app_whitelist",
|
||||
name: "Whitelist Apps for Capture",
|
||||
domain: "screen_intelligence",
|
||||
category: CapabilityCategory::ScreenIntelligence,
|
||||
description: "Allow screen intelligence only for selected applications.",
|
||||
how_to: "Settings > Screen Intelligence",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "screen_intelligence.app_blacklist",
|
||||
name: "Blacklist Apps from Capture",
|
||||
domain: "screen_intelligence",
|
||||
category: CapabilityCategory::ScreenIntelligence,
|
||||
description: "Exclude selected applications from screen intelligence capture.",
|
||||
how_to: "Settings > Screen Intelligence",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "channels.connect_platform",
|
||||
name: "Connect Messaging Platforms",
|
||||
domain: "channels",
|
||||
category: CapabilityCategory::Channels,
|
||||
description: "Connect supported messaging platforms such as Telegram, Discord, or Slack.",
|
||||
how_to: "Settings > Messaging Channels",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "channels.disconnect_platform",
|
||||
name: "Disconnect Messaging Platforms",
|
||||
domain: "channels",
|
||||
category: CapabilityCategory::Channels,
|
||||
description: "Disconnect a previously configured messaging platform.",
|
||||
how_to: "Settings > Messaging Channels",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "channels.test_credentials",
|
||||
name: "Test Channel Credentials",
|
||||
domain: "channels",
|
||||
category: CapabilityCategory::Channels,
|
||||
description: "Validate platform credentials or connection state before using a channel.",
|
||||
how_to: "Settings > Messaging Channels",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "channels.set_default_channel",
|
||||
name: "Set Default Messaging Channel",
|
||||
domain: "channels",
|
||||
category: CapabilityCategory::Channels,
|
||||
description: "Choose which messaging channel should be used by default.",
|
||||
how_to: "Settings > Messaging Channels",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "settings.configure_ai",
|
||||
name: "Configure AI",
|
||||
domain: "settings",
|
||||
category: CapabilityCategory::Settings,
|
||||
description: "Adjust AI-related settings and agent behavior preferences.",
|
||||
how_to: "Settings > Developer Options > AI Configuration",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "settings.manage_privacy_analytics",
|
||||
name: "Manage Privacy and Analytics",
|
||||
domain: "settings",
|
||||
category: CapabilityCategory::Settings,
|
||||
description: "Control privacy, analytics, and related data handling preferences.",
|
||||
how_to: "Settings > Privacy (direct route)",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "settings.view_billing",
|
||||
name: "View Billing",
|
||||
domain: "settings",
|
||||
category: CapabilityCategory::Settings,
|
||||
description: "Open billing and usage views for your active team.",
|
||||
how_to: "Settings > Billing & Usage",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "settings.manage_subscription_plan",
|
||||
name: "Manage Subscription Plan",
|
||||
domain: "settings",
|
||||
category: CapabilityCategory::Settings,
|
||||
description: "Upgrade plans or open the billing portal to manage subscriptions.",
|
||||
how_to: "Settings > Billing & Usage",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "settings.manage_credits",
|
||||
name: "Manage Credits",
|
||||
domain: "settings",
|
||||
category: CapabilityCategory::Settings,
|
||||
description: "View credit balances, top up credits, and configure auto-recharge.",
|
||||
how_to: "Settings > Billing & Usage",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "settings.add_payment_methods",
|
||||
name: "Add Payment Methods",
|
||||
domain: "settings",
|
||||
category: CapabilityCategory::Settings,
|
||||
description: "Add or manage saved payment methods for billing and auto-recharge.",
|
||||
how_to: "Settings > Billing & Usage > Payment Methods",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "settings.developer_options",
|
||||
name: "Developer Options",
|
||||
domain: "settings",
|
||||
category: CapabilityCategory::Settings,
|
||||
description: "Open developer-focused panels for diagnostics, skills, AI config, and memory tools.",
|
||||
how_to: "Settings > Developer Options",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
Capability {
|
||||
id: "settings.manage_service",
|
||||
name: "Manage Desktop Service",
|
||||
domain: "settings",
|
||||
category: CapabilityCategory::Settings,
|
||||
description: "Install, start, stop, restart, uninstall, or refresh the required desktop service.",
|
||||
how_to: "App startup > OpenHuman Service Required",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "settings.clear_app_data",
|
||||
name: "Log Out and Clear App Data",
|
||||
domain: "settings",
|
||||
category: CapabilityCategory::Settings,
|
||||
description: "Sign out and permanently clear local app data, including skills data.",
|
||||
how_to: "Settings > Log Out & Clear App Data",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "settings.delete_all_data",
|
||||
name: "Delete All Data",
|
||||
domain: "settings",
|
||||
category: CapabilityCategory::Settings,
|
||||
description: "Delete all local data and reset the app from the destructive settings section.",
|
||||
how_to: "Settings > Delete All Data",
|
||||
status: CapabilityStatus::ComingSoon,
|
||||
},
|
||||
Capability {
|
||||
id: "automation.view_cron_jobs",
|
||||
name: "View Cron Jobs",
|
||||
domain: "automation",
|
||||
category: CapabilityCategory::Automation,
|
||||
description: "Review scheduled jobs available to the runtime.",
|
||||
how_to: "Settings > Cron Jobs",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "automation.set_job_intervals",
|
||||
name: "Set Job Intervals",
|
||||
domain: "automation",
|
||||
category: CapabilityCategory::Automation,
|
||||
description: "Configure how often a scheduled job should run.",
|
||||
how_to: "Settings > Cron Jobs",
|
||||
status: CapabilityStatus::Stable,
|
||||
},
|
||||
Capability {
|
||||
id: "automation.view_execution_history",
|
||||
name: "View Execution History",
|
||||
domain: "automation",
|
||||
category: CapabilityCategory::Automation,
|
||||
description: "Inspect past runs and results for scheduled jobs.",
|
||||
how_to: "Settings > Cron Jobs",
|
||||
status: CapabilityStatus::Beta,
|
||||
},
|
||||
];
|
||||
|
||||
static VALIDATED: OnceLock<()> = OnceLock::new();
|
||||
|
||||
pub fn all_capabilities() -> &'static [Capability] {
|
||||
ensure_validated();
|
||||
CAPABILITIES
|
||||
}
|
||||
|
||||
pub fn capabilities_by_category(category: CapabilityCategory) -> Vec<Capability> {
|
||||
ensure_validated();
|
||||
CAPABILITIES
|
||||
.iter()
|
||||
.filter(|capability| capability.category == category)
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn lookup(id: &str) -> Option<Capability> {
|
||||
ensure_validated();
|
||||
let normalized = id.trim();
|
||||
CAPABILITIES
|
||||
.iter()
|
||||
.find(|capability| capability.id == normalized)
|
||||
.copied()
|
||||
}
|
||||
|
||||
pub fn search(query: &str) -> Vec<Capability> {
|
||||
ensure_validated();
|
||||
let normalized = query.trim().to_ascii_lowercase();
|
||||
if normalized.is_empty() {
|
||||
return CAPABILITIES.to_vec();
|
||||
}
|
||||
|
||||
CAPABILITIES
|
||||
.iter()
|
||||
.filter(|capability| searchable_text(capability).contains(&normalized))
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn searchable_text(capability: &Capability) -> String {
|
||||
format!(
|
||||
"{} {} {} {} {} {} {}",
|
||||
capability.id,
|
||||
capability.name,
|
||||
capability.domain,
|
||||
capability.category.as_str(),
|
||||
capability.description,
|
||||
capability.how_to,
|
||||
capability.status.as_str()
|
||||
)
|
||||
.to_ascii_lowercase()
|
||||
}
|
||||
|
||||
fn ensure_validated() {
|
||||
VALIDATED.get_or_init(|| {
|
||||
let mut ids = BTreeSet::new();
|
||||
for capability in CAPABILITIES {
|
||||
assert!(
|
||||
!capability.id.trim().is_empty(),
|
||||
"about_app capability id must not be empty"
|
||||
);
|
||||
assert!(
|
||||
ids.insert(capability.id),
|
||||
"duplicate about_app capability id '{}'",
|
||||
capability.id
|
||||
);
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
count = CAPABILITIES.len(),
|
||||
"[about_app] validated capability catalog"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn lookup_returns_expected_capability() {
|
||||
let capability = lookup("local_ai.download_model").expect("capability should exist");
|
||||
assert_eq!(capability.category, CapabilityCategory::LocalAI);
|
||||
assert_eq!(capability.status, CapabilityStatus::Beta);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_matches_keyword_across_multiple_fields() {
|
||||
let matches = search("invite");
|
||||
let ids: Vec<&str> = matches.iter().map(|capability| capability.id).collect();
|
||||
|
||||
assert!(ids.contains(&"team.join_via_invite_code"));
|
||||
assert!(ids.contains(&"team.generate_invite_codes"));
|
||||
assert!(ids.contains(&"team.track_invite_usage"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_ids_are_unique() {
|
||||
let ids: BTreeSet<&str> = all_capabilities()
|
||||
.iter()
|
||||
.map(|capability| capability.id)
|
||||
.collect();
|
||||
assert_eq!(ids.len(), all_capabilities().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn category_filter_returns_matching_entries() {
|
||||
let capabilities = capabilities_by_category(CapabilityCategory::Automation);
|
||||
assert!(capabilities
|
||||
.iter()
|
||||
.all(|capability| { capability.category == CapabilityCategory::Automation }));
|
||||
assert!(!capabilities.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catalog_includes_additional_user_facing_surfaces() {
|
||||
let ids: BTreeSet<&str> = all_capabilities()
|
||||
.iter()
|
||||
.map(|capability| capability.id)
|
||||
.collect();
|
||||
|
||||
for expected in [
|
||||
"skills.open_connections_hub",
|
||||
"skills.connect_google",
|
||||
"auth.backup_recovery_phrase",
|
||||
"auth.configure_tool_access",
|
||||
"settings.manage_service",
|
||||
"settings.clear_app_data",
|
||||
] {
|
||||
assert!(
|
||||
ids.contains(expected),
|
||||
"missing catalog capability `{expected}`"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//! User-facing capability catalog for the OpenHuman app.
|
||||
//!
|
||||
//! This module is the single source of truth for what the desktop app exposes
|
||||
//! to end users, including where a capability lives in the UI and whether it is
|
||||
//! stable, beta, coming soon, or deprecated.
|
||||
|
||||
mod catalog;
|
||||
mod schemas;
|
||||
mod types;
|
||||
|
||||
use crate::rpc::RpcOutcome;
|
||||
|
||||
pub use catalog::{all_capabilities, capabilities_by_category, lookup, search};
|
||||
pub use schemas::{
|
||||
about_app_schemas, all_about_app_controller_schemas, all_about_app_registered_controllers,
|
||||
};
|
||||
pub use types::{Capability, CapabilityCategory, CapabilityStatus};
|
||||
|
||||
pub fn list_capabilities(category: Option<CapabilityCategory>) -> RpcOutcome<Vec<Capability>> {
|
||||
let capabilities = match category {
|
||||
Some(category) => capabilities_by_category(category),
|
||||
None => all_capabilities().to_vec(),
|
||||
};
|
||||
let log = format!(
|
||||
"about_app.list returned {} capabilities",
|
||||
capabilities.len()
|
||||
);
|
||||
RpcOutcome::single_log(capabilities, log)
|
||||
}
|
||||
|
||||
pub fn lookup_capability(id: &str) -> Result<RpcOutcome<Capability>, String> {
|
||||
let capability = lookup(id).ok_or_else(|| format!("unknown capability id '{}'", id.trim()))?;
|
||||
Ok(RpcOutcome::single_log(
|
||||
capability,
|
||||
format!("about_app.lookup returned {}", capability.id),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn search_capabilities(query: &str) -> RpcOutcome<Vec<Capability>> {
|
||||
let capabilities = search(query);
|
||||
let log = format!(
|
||||
"about_app.search returned {} capabilities for '{}'",
|
||||
capabilities.len(),
|
||||
query.trim()
|
||||
);
|
||||
RpcOutcome::single_log(capabilities, log)
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use crate::core::all::{ControllerFuture, RegisteredController};
|
||||
use crate::core::{ControllerSchema, FieldSchema, TypeSchema};
|
||||
use crate::openhuman::about_app::CapabilityCategory;
|
||||
use crate::rpc::RpcOutcome;
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
struct AboutAppListParams {
|
||||
#[serde(default)]
|
||||
category: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AboutAppLookupParams {
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AboutAppSearchParams {
|
||||
query: String,
|
||||
}
|
||||
|
||||
pub fn all_about_app_controller_schemas() -> Vec<ControllerSchema> {
|
||||
vec![
|
||||
about_app_schemas("about_app_list"),
|
||||
about_app_schemas("about_app_lookup"),
|
||||
about_app_schemas("about_app_search"),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn all_about_app_registered_controllers() -> Vec<RegisteredController> {
|
||||
vec![
|
||||
RegisteredController {
|
||||
schema: about_app_schemas("about_app_list"),
|
||||
handler: handle_about_app_list,
|
||||
},
|
||||
RegisteredController {
|
||||
schema: about_app_schemas("about_app_lookup"),
|
||||
handler: handle_about_app_lookup,
|
||||
},
|
||||
RegisteredController {
|
||||
schema: about_app_schemas("about_app_search"),
|
||||
handler: handle_about_app_search,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
pub fn about_app_schemas(function: &str) -> ControllerSchema {
|
||||
match function {
|
||||
"about_app_list" => ControllerSchema {
|
||||
namespace: "about_app",
|
||||
function: "list",
|
||||
description: "List all user-facing app capabilities, optionally filtered by category.",
|
||||
inputs: vec![optional_category(
|
||||
"category",
|
||||
"Optional capability category filter.",
|
||||
)],
|
||||
outputs: vec![capabilities_output(
|
||||
"capabilities",
|
||||
"Capability catalog entries matching the list filter.",
|
||||
)],
|
||||
},
|
||||
"about_app_lookup" => ControllerSchema {
|
||||
namespace: "about_app",
|
||||
function: "lookup",
|
||||
description: "Look up one user-facing capability by its stable id.",
|
||||
inputs: vec![required_string(
|
||||
"id",
|
||||
"Capability id, such as local_ai.download_model.",
|
||||
)],
|
||||
outputs: vec![capability_output(
|
||||
"capability",
|
||||
"One capability entry for the requested id.",
|
||||
)],
|
||||
},
|
||||
"about_app_search" => ControllerSchema {
|
||||
namespace: "about_app",
|
||||
function: "search",
|
||||
description: "Search user-facing capabilities by keyword.",
|
||||
inputs: vec![required_string("query", "Keyword query to search for.")],
|
||||
outputs: vec![capabilities_output(
|
||||
"capabilities",
|
||||
"Capability catalog entries matching the search query.",
|
||||
)],
|
||||
},
|
||||
_ => ControllerSchema {
|
||||
namespace: "about_app",
|
||||
function: "unknown",
|
||||
description: "Unknown about_app controller.",
|
||||
inputs: vec![],
|
||||
outputs: vec![FieldSchema {
|
||||
name: "error",
|
||||
ty: TypeSchema::String,
|
||||
comment: "Lookup error details.",
|
||||
required: true,
|
||||
}],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_about_app_list(params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move {
|
||||
let payload = deserialize_params::<AboutAppListParams>(params)?;
|
||||
let category = payload
|
||||
.category
|
||||
.as_deref()
|
||||
.map(CapabilityCategory::from_str)
|
||||
.transpose()?;
|
||||
|
||||
tracing::debug!(?category, "[about_app] list capabilities");
|
||||
to_json(crate::openhuman::about_app::list_capabilities(category))
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_about_app_lookup(params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move {
|
||||
let payload = deserialize_params::<AboutAppLookupParams>(params)?;
|
||||
tracing::debug!(id = %payload.id, "[about_app] lookup capability");
|
||||
to_json(crate::openhuman::about_app::lookup_capability(&payload.id)?)
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_about_app_search(params: Map<String, Value>) -> ControllerFuture {
|
||||
Box::pin(async move {
|
||||
let payload = deserialize_params::<AboutAppSearchParams>(params)?;
|
||||
tracing::debug!(query = %payload.query, "[about_app] search capabilities");
|
||||
to_json(crate::openhuman::about_app::search_capabilities(
|
||||
&payload.query,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn deserialize_params<T: DeserializeOwned>(params: Map<String, Value>) -> Result<T, String> {
|
||||
serde_json::from_value(Value::Object(params)).map_err(|e| format!("invalid params: {e}"))
|
||||
}
|
||||
|
||||
fn to_json<T: serde::Serialize>(outcome: RpcOutcome<T>) -> Result<Value, String> {
|
||||
outcome.into_cli_compatible_json()
|
||||
}
|
||||
|
||||
fn required_string(name: &'static str, comment: &'static str) -> FieldSchema {
|
||||
FieldSchema {
|
||||
name,
|
||||
ty: TypeSchema::String,
|
||||
comment,
|
||||
required: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn optional_category(name: &'static str, comment: &'static str) -> FieldSchema {
|
||||
FieldSchema {
|
||||
name,
|
||||
ty: TypeSchema::Option(Box::new(TypeSchema::Enum {
|
||||
variants: CapabilityCategory::ALL
|
||||
.iter()
|
||||
.map(|category| category.as_str())
|
||||
.collect(),
|
||||
})),
|
||||
comment,
|
||||
required: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn capability_output(name: &'static str, comment: &'static str) -> FieldSchema {
|
||||
FieldSchema {
|
||||
name,
|
||||
ty: TypeSchema::Ref("Capability"),
|
||||
comment,
|
||||
required: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn capabilities_output(name: &'static str, comment: &'static str) -> FieldSchema {
|
||||
FieldSchema {
|
||||
name,
|
||||
ty: TypeSchema::Array(Box::new(TypeSchema::Ref("Capability"))),
|
||||
comment,
|
||||
required: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn schema_names_are_stable() {
|
||||
let list = about_app_schemas("about_app_list");
|
||||
assert_eq!(list.namespace, "about_app");
|
||||
assert_eq!(list.function, "list");
|
||||
|
||||
let lookup = about_app_schemas("about_app_lookup");
|
||||
assert_eq!(lookup.namespace, "about_app");
|
||||
assert_eq!(lookup.function, "lookup");
|
||||
|
||||
let search = about_app_schemas("about_app_search");
|
||||
assert_eq!(search.namespace, "about_app");
|
||||
assert_eq!(search.function, "search");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controller_lists_match_lengths() {
|
||||
assert_eq!(
|
||||
all_about_app_controller_schemas().len(),
|
||||
all_about_app_registered_controllers().len()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum CapabilityCategory {
|
||||
#[serde(rename = "conversation")]
|
||||
Conversation,
|
||||
#[serde(rename = "intelligence")]
|
||||
Intelligence,
|
||||
#[serde(rename = "skills")]
|
||||
Skills,
|
||||
#[serde(rename = "local_ai")]
|
||||
LocalAI,
|
||||
#[serde(rename = "team")]
|
||||
Team,
|
||||
#[serde(rename = "settings")]
|
||||
Settings,
|
||||
#[serde(rename = "auth")]
|
||||
Auth,
|
||||
#[serde(rename = "screen_intelligence")]
|
||||
ScreenIntelligence,
|
||||
#[serde(rename = "channels")]
|
||||
Channels,
|
||||
#[serde(rename = "automation")]
|
||||
Automation,
|
||||
}
|
||||
|
||||
impl CapabilityCategory {
|
||||
pub const ALL: [Self; 10] = [
|
||||
Self::Conversation,
|
||||
Self::Intelligence,
|
||||
Self::Skills,
|
||||
Self::LocalAI,
|
||||
Self::Team,
|
||||
Self::Settings,
|
||||
Self::Auth,
|
||||
Self::ScreenIntelligence,
|
||||
Self::Channels,
|
||||
Self::Automation,
|
||||
];
|
||||
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Conversation => "conversation",
|
||||
Self::Intelligence => "intelligence",
|
||||
Self::Skills => "skills",
|
||||
Self::LocalAI => "local_ai",
|
||||
Self::Team => "team",
|
||||
Self::Settings => "settings",
|
||||
Self::Auth => "auth",
|
||||
Self::ScreenIntelligence => "screen_intelligence",
|
||||
Self::Channels => "channels",
|
||||
Self::Automation => "automation",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CapabilityCategory {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
let normalized = value.trim().to_ascii_lowercase();
|
||||
match normalized.as_str() {
|
||||
"conversation" => Ok(Self::Conversation),
|
||||
"intelligence" => Ok(Self::Intelligence),
|
||||
"skills" => Ok(Self::Skills),
|
||||
"local_ai" | "local-ai" | "local ai" | "localai" => Ok(Self::LocalAI),
|
||||
"team" => Ok(Self::Team),
|
||||
"settings" => Ok(Self::Settings),
|
||||
"auth" => Ok(Self::Auth),
|
||||
"screen_intelligence" | "screen-intelligence" | "screen intelligence" => {
|
||||
Ok(Self::ScreenIntelligence)
|
||||
}
|
||||
"channels" => Ok(Self::Channels),
|
||||
"automation" => Ok(Self::Automation),
|
||||
_ => Err(format!(
|
||||
"unknown capability category '{value}'; expected one of: {}",
|
||||
Self::ALL
|
||||
.iter()
|
||||
.map(|category| category.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum CapabilityStatus {
|
||||
#[serde(rename = "stable")]
|
||||
Stable,
|
||||
#[serde(rename = "beta")]
|
||||
Beta,
|
||||
#[serde(rename = "coming_soon")]
|
||||
ComingSoon,
|
||||
#[serde(rename = "deprecated")]
|
||||
Deprecated,
|
||||
}
|
||||
|
||||
impl CapabilityStatus {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Stable => "stable",
|
||||
Self::Beta => "beta",
|
||||
Self::ComingSoon => "coming_soon",
|
||||
Self::Deprecated => "deprecated",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
pub struct Capability {
|
||||
pub id: &'static str,
|
||||
pub name: &'static str,
|
||||
pub domain: &'static str,
|
||||
pub category: CapabilityCategory,
|
||||
pub description: &'static str,
|
||||
pub how_to: &'static str,
|
||||
pub status: CapabilityStatus,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn category_serializes_expected_wire_names() {
|
||||
assert_eq!(
|
||||
serde_json::to_string(&CapabilityCategory::LocalAI).expect("serialize LocalAI"),
|
||||
"\"local_ai\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&CapabilityCategory::ScreenIntelligence)
|
||||
.expect("serialize ScreenIntelligence"),
|
||||
"\"screen_intelligence\""
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_serializes_expected_wire_names() {
|
||||
assert_eq!(
|
||||
serde_json::to_string(&CapabilityStatus::ComingSoon).expect("serialize ComingSoon"),
|
||||
"\"coming_soon\""
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
// Many types/functions are not yet consumed but are intentionally exported.
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub mod about_app;
|
||||
pub mod accessibility;
|
||||
pub mod agent;
|
||||
pub mod approval;
|
||||
|
||||
@@ -1554,6 +1554,115 @@ async fn team_rpc_e2e() {
|
||||
rpc_join.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn about_app_rpc_list_lookup_and_search() {
|
||||
let _env_lock = json_rpc_e2e_env_lock();
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let home = tmp.path();
|
||||
let openhuman_home = home.join(".openhuman");
|
||||
|
||||
let _home_guard = EnvVarGuard::set_to_path("HOME", home);
|
||||
let _workspace_guard = EnvVarGuard::unset("OPENHUMAN_WORKSPACE");
|
||||
let _backend_url_guard = EnvVarGuard::unset("BACKEND_URL");
|
||||
let _vite_backend_guard = EnvVarGuard::unset("VITE_BACKEND_URL");
|
||||
|
||||
let (mock_addr, mock_join) = serve_on_ephemeral(mock_upstream_router()).await;
|
||||
let mock_origin = format!("http://{}", mock_addr);
|
||||
write_min_config(&openhuman_home, &mock_origin);
|
||||
|
||||
let (rpc_addr, rpc_join) = serve_on_ephemeral(build_core_http_router(false)).await;
|
||||
let rpc_base = format!("http://{}", rpc_addr);
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
|
||||
fn inner(outer: &Value) -> Value {
|
||||
outer
|
||||
.get("result")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| outer.clone())
|
||||
}
|
||||
|
||||
let list = post_json_rpc(&rpc_base, 200, "openhuman.about_app_list", json!({})).await;
|
||||
let list_outer = assert_no_jsonrpc_error(&list, "about_app_list");
|
||||
let list_result = inner(list_outer);
|
||||
let capabilities = list_result
|
||||
.as_array()
|
||||
.expect("about_app list should return an array");
|
||||
assert!(
|
||||
capabilities.len() >= 40,
|
||||
"expected large capability catalog, got: {list_result}"
|
||||
);
|
||||
assert!(capabilities.iter().any(|capability| {
|
||||
capability.get("id").and_then(Value::as_str) == Some("local_ai.download_model")
|
||||
}));
|
||||
|
||||
let filtered = post_json_rpc(
|
||||
&rpc_base,
|
||||
201,
|
||||
"openhuman.about_app_list",
|
||||
json!({ "category": "local_ai" }),
|
||||
)
|
||||
.await;
|
||||
let filtered_outer = assert_no_jsonrpc_error(&filtered, "about_app_list filtered");
|
||||
let filtered_result = inner(filtered_outer);
|
||||
let filtered_capabilities = filtered_result
|
||||
.as_array()
|
||||
.expect("filtered about_app list should return an array");
|
||||
assert!(
|
||||
!filtered_capabilities.is_empty(),
|
||||
"expected local_ai capabilities: {filtered_result}"
|
||||
);
|
||||
assert!(filtered_capabilities.iter().all(|capability| {
|
||||
capability.get("category").and_then(Value::as_str) == Some("local_ai")
|
||||
}));
|
||||
|
||||
let lookup = post_json_rpc(
|
||||
&rpc_base,
|
||||
202,
|
||||
"openhuman.about_app_lookup",
|
||||
json!({ "id": "team.generate_invite_codes" }),
|
||||
)
|
||||
.await;
|
||||
let lookup_outer = assert_no_jsonrpc_error(&lookup, "about_app_lookup");
|
||||
let lookup_result = inner(lookup_outer);
|
||||
assert_eq!(
|
||||
lookup_result.get("id").and_then(Value::as_str),
|
||||
Some("team.generate_invite_codes")
|
||||
);
|
||||
assert_eq!(
|
||||
lookup_result.get("category").and_then(Value::as_str),
|
||||
Some("team")
|
||||
);
|
||||
|
||||
let search = post_json_rpc(
|
||||
&rpc_base,
|
||||
203,
|
||||
"openhuman.about_app_search",
|
||||
json!({ "query": "invite" }),
|
||||
)
|
||||
.await;
|
||||
let search_outer = assert_no_jsonrpc_error(&search, "about_app_search");
|
||||
let search_result = inner(search_outer);
|
||||
let search_capabilities = search_result
|
||||
.as_array()
|
||||
.expect("about_app search should return an array");
|
||||
assert!(
|
||||
search_capabilities.iter().any(|capability| {
|
||||
capability.get("id").and_then(Value::as_str) == Some("team.join_via_invite_code")
|
||||
}),
|
||||
"expected invite-related capability in search results: {search_result}"
|
||||
);
|
||||
assert!(
|
||||
search_capabilities.iter().any(|capability| {
|
||||
capability.get("id").and_then(Value::as_str) == Some("team.generate_invite_codes")
|
||||
}),
|
||||
"expected invite generation capability in search results: {search_result}"
|
||||
);
|
||||
|
||||
mock_join.abort();
|
||||
rpc_join.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn voice_status_returns_availability() {
|
||||
let _env_lock = json_rpc_e2e_env_lock();
|
||||
|
||||
Reference in New Issue
Block a user