mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
chore: update skills submodule to latest commit and enhance SkillsGrid component
- Updated the skills submodule to the latest commit for improved functionality. - Refactored `SkillsGrid` to better handle skill setup logic, ensuring accurate representation of skills with setup requirements. - Cleaned up code formatting and improved readability in various components, including `SkillActionButton` and `desktopDeepLinkListener`.
This commit is contained in:
+1
-1
Submodule skills updated: 66ad4d7e3f...66ec30016f
@@ -185,6 +185,9 @@ pub struct UnifiedSkillEntry {
|
||||
pub description: String,
|
||||
/// Tools exposed by this skill.
|
||||
pub tools: Vec<ToolDefinition>,
|
||||
/// Setup configuration from manifest.json (e.g. whether setup is required).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub setup: Option<crate::runtime::manifest::SkillSetup>,
|
||||
}
|
||||
|
||||
/// Standardized result returned by any skill execution in the unified registry.
|
||||
|
||||
@@ -91,6 +91,7 @@ impl UnifiedSkillRegistry {
|
||||
version: manifest.version.clone().unwrap_or_else(|| "0.1.0".to_string()),
|
||||
description: manifest.description.clone().unwrap_or_default(),
|
||||
tools,
|
||||
setup: manifest.setup.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -126,6 +127,7 @@ impl UnifiedSkillRegistry {
|
||||
version: skill.version.clone(),
|
||||
description: skill.description.clone(),
|
||||
tools,
|
||||
setup: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -208,6 +210,7 @@ impl UnifiedSkillRegistry {
|
||||
version: "1.0.0".to_string(),
|
||||
description: spec.description,
|
||||
tools: vec![],
|
||||
setup: None,
|
||||
})
|
||||
}
|
||||
"openclaw" => {
|
||||
@@ -221,6 +224,7 @@ impl UnifiedSkillRegistry {
|
||||
version: "1.0.0".to_string(),
|
||||
description: spec.description,
|
||||
tools: vec![],
|
||||
setup: None,
|
||||
})
|
||||
}
|
||||
other => Err(format!("Unknown skill_type: '{other}'. Use 'alphahuman' or 'openclaw'.")),
|
||||
|
||||
@@ -19,16 +19,24 @@ import SkillSetupModal from './skills/SkillSetupModal';
|
||||
|
||||
/** Normalize a raw unified registry entry into a SkillListEntry for display. */
|
||||
function normalizeUnifiedEntry(e: Record<string, unknown>): SkillListEntry {
|
||||
const setup = e.setup as Record<string, unknown> | undefined;
|
||||
const setup = e.setup as { required?: boolean; oauth?: unknown } | undefined;
|
||||
// Treat both interactive setup steps and OAuth-only flows as "has setup"
|
||||
// so that clicking a skill (e.g. Gmail) opens the connection/setup wizard
|
||||
// instead of jumping straight to the management panel.
|
||||
const hasSetup =
|
||||
!!setup &&
|
||||
(setup.required === true ||
|
||||
// OAuth config means we still need a connection step in the wizard
|
||||
!!setup.oauth);
|
||||
|
||||
return {
|
||||
id: e.id as string,
|
||||
name:
|
||||
(e.name as string) ||
|
||||
(e.id as string).charAt(0).toUpperCase() + (e.id as string).slice(1),
|
||||
(e.name as string) || (e.id as string).charAt(0).toUpperCase() + (e.id as string).slice(1),
|
||||
description: (e.description as string) || '',
|
||||
icon: SKILL_ICONS[e.id as string],
|
||||
ignoreInProduction: (e.ignoreInProduction as boolean) ?? false,
|
||||
hasSetup: !!(setup && setup.required),
|
||||
hasSetup,
|
||||
skill_type: (e.skill_type as 'alphahuman' | 'openclaw') ?? 'alphahuman',
|
||||
};
|
||||
}
|
||||
@@ -47,9 +55,7 @@ function SkillTypeBadge({ type }: { type?: string }) {
|
||||
return (
|
||||
<span
|
||||
className={`text-[10px] font-medium px-1.5 py-0.5 rounded-md ${
|
||||
isOpenclaw
|
||||
? 'bg-sage-500/15 text-sage-400'
|
||||
: 'bg-primary-500/15 text-primary-400'
|
||||
isOpenclaw ? 'bg-sage-500/15 text-sage-400' : 'bg-primary-500/15 text-primary-400'
|
||||
}`}>
|
||||
{type}
|
||||
</span>
|
||||
@@ -149,14 +155,19 @@ export default function SkillsGrid() {
|
||||
const processed: SkillListEntry[] = manifests
|
||||
.filter(m => !(m.id as string).includes('_'))
|
||||
.map(m => {
|
||||
const setup = m.setup as Record<string, unknown> | undefined;
|
||||
const setup = m.setup as { required?: boolean; oauth?: unknown } | undefined;
|
||||
const hasSetup =
|
||||
!!setup &&
|
||||
(setup.required === true ||
|
||||
// OAuth-only skills still need a setup/connect flow
|
||||
!!setup.oauth);
|
||||
return {
|
||||
id: m.id as string,
|
||||
name: (m.name as string) || (m.id as string),
|
||||
description: (m.description as string) || '',
|
||||
icon: SKILL_ICONS[m.id as string],
|
||||
ignoreInProduction: (m.ignoreInProduction as boolean) ?? false,
|
||||
hasSetup: !!(setup && setup.required),
|
||||
hasSetup,
|
||||
skill_type: 'alphahuman' as const,
|
||||
};
|
||||
})
|
||||
@@ -183,7 +194,6 @@ export default function SkillsGrid() {
|
||||
};
|
||||
detectMobile();
|
||||
refreshSkills();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Sort skills by connection status (connected first)
|
||||
@@ -270,11 +280,7 @@ export default function SkillsGrid() {
|
||||
}}
|
||||
className="text-xs text-primary-400 hover:text-primary-300 transition-colors flex items-center gap-1">
|
||||
{/* Sparkle / robot icon */}
|
||||
<svg
|
||||
className="w-3.5 h-3.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -296,7 +302,7 @@ export default function SkillsGrid() {
|
||||
description: 'Auto-generated skill demonstrating the unified registry',
|
||||
skill_type: 'alphahuman',
|
||||
tool_code:
|
||||
"return { message: `Hello from generated skill! args=${JSON.stringify(args)}` };",
|
||||
'return { message: `Hello from generated skill! args=${JSON.stringify(args)}` };',
|
||||
},
|
||||
});
|
||||
await refreshSkills();
|
||||
@@ -312,7 +318,11 @@ export default function SkillsGrid() {
|
||||
<span className="opacity-60">Generating…</span>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg
|
||||
className="w-3.5 h-3.5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -387,10 +397,7 @@ export default function SkillsGrid() {
|
||||
|
||||
{/* Self-Evolve modal */}
|
||||
{selfEvolveOpen && (
|
||||
<SelfEvolveModal
|
||||
onClose={() => setSelfEvolveOpen(false)}
|
||||
onSkillCreated={refreshSkills}
|
||||
/>
|
||||
<SelfEvolveModal onClose={() => setSelfEvolveOpen(false)} onSkillCreated={refreshSkills} />
|
||||
)}
|
||||
|
||||
{/* Skills Management Modal */}
|
||||
|
||||
@@ -92,6 +92,7 @@ export function SkillActionButton({
|
||||
version: '0.0.0',
|
||||
description: skill.description,
|
||||
runtime: 'quickjs',
|
||||
setup: skill.hasSetup ? { required: true } : undefined,
|
||||
});
|
||||
if (skill.hasSetup) {
|
||||
onOpenModal();
|
||||
|
||||
@@ -9,43 +9,26 @@ const INTEGRATION_METADATA_SYNC_EVENT = 'integration:metadata-sync';
|
||||
const PROVIDER_GOOGLE = 'gmail';
|
||||
|
||||
/** Gmail profile shape from skill state (snake_case). */
|
||||
interface GmailProfileLike {
|
||||
export interface GmailProfileLike {
|
||||
email_address: string;
|
||||
messages_total: number;
|
||||
threads_total: number;
|
||||
history_id: string;
|
||||
}
|
||||
|
||||
/** Single email summary from skill state. */
|
||||
interface GmailEmailSummaryLike {
|
||||
id: string;
|
||||
threadId: string;
|
||||
snippet?: string;
|
||||
subject?: string;
|
||||
from?: string;
|
||||
date?: string;
|
||||
}
|
||||
|
||||
/** Gmail skill state slice we care about for metadata sync. */
|
||||
export interface GmailStateForSync {
|
||||
profile?: GmailProfileLike | null;
|
||||
emails?: GmailEmailSummaryLike[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit `integration:metadata-sync` with Gmail profile and emails so the
|
||||
* backend can merge them into the user's Google OAuth integration.
|
||||
* No-op when profile is missing or not in Tauri.
|
||||
*/
|
||||
export function syncGmailMetadataToBackend(gmailState: GmailStateForSync | undefined): void {
|
||||
if (!gmailState?.profile || typeof gmailState.profile !== 'object') return;
|
||||
export function syncGmailMetadataToBackend(gmailState: GmailProfileLike | undefined): void {
|
||||
if (!gmailState) return;
|
||||
|
||||
const profile = gmailState.profile as GmailProfileLike;
|
||||
const metadata: Record<string, unknown> = {
|
||||
email_address: profile.email_address,
|
||||
messages_total: profile.messages_total,
|
||||
threads_total: profile.threads_total,
|
||||
history_id: profile.history_id,
|
||||
email_address: gmailState.email_address,
|
||||
messages_total: gmailState.messages_total,
|
||||
threads_total: gmailState.threads_total,
|
||||
history_id: gmailState.history_id,
|
||||
};
|
||||
|
||||
// if (Array.isArray(gmailState.emails) && gmailState.emails.length > 0) {
|
||||
|
||||
@@ -81,7 +81,14 @@ class SkillManager {
|
||||
// Dead runtime — clean up
|
||||
this.runtimes.delete(skillId);
|
||||
}
|
||||
|
||||
// Ensure the skill is registered in Redux before dispatching status updates.
|
||||
// Self-evolved skills are started directly by the Rust engine and never go
|
||||
// through registerSkill(), so state.skills[skillId] is undefined. Every
|
||||
// setSkillStatus / setSkillSetupComplete / setSkillTools reducer silently
|
||||
// no-ops when the key is missing, making the Enable button appear broken.
|
||||
if (!store.getState().skills.skills[skillId]) {
|
||||
store.dispatch(addSkill({ manifest }));
|
||||
}
|
||||
store.dispatch(setSkillStatus({ skillId, status: "starting" }));
|
||||
|
||||
const runtime = new SkillRuntime(manifest);
|
||||
|
||||
@@ -9,18 +9,13 @@ import { listen } from '@tauri-apps/api/event';
|
||||
import { type ReactNode, useEffect, useRef } from 'react';
|
||||
|
||||
import {
|
||||
type GmailStateForSync,
|
||||
type GmailProfileLike,
|
||||
syncGmailMetadataToBackend,
|
||||
} from '../lib/gmail/services/metadataSync';
|
||||
import { skillManager } from '../lib/skills/manager';
|
||||
import type { SkillManifest } from '../lib/skills/types';
|
||||
import { buildManualSentryEvent, enqueueError } from '../services/errorReportQueue';
|
||||
import {
|
||||
type GmailEmailSummary,
|
||||
type GmailProfile,
|
||||
setGmailEmails,
|
||||
setGmailProfile,
|
||||
} from '../store/gmailSlice';
|
||||
import { type GmailProfile, setGmailProfile } from '../store/gmailSlice';
|
||||
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||
import { setSkillError, setSkillState, setSkillStatus } from '../store/skillsSlice';
|
||||
import { DEV_AUTO_LOAD_SKILL, IS_DEV } from '../utils/config';
|
||||
@@ -88,12 +83,7 @@ function syncGmailStateToSlice(
|
||||
: null
|
||||
)
|
||||
);
|
||||
dispatch(
|
||||
setGmailEmails(
|
||||
Array.isArray(gmailState.emails) ? (gmailState.emails as GmailEmailSummary[]) : []
|
||||
)
|
||||
);
|
||||
syncGmailMetadataToBackend(gmailState.profile as GmailStateForSync);
|
||||
syncGmailMetadataToBackend(gmailState.profile as GmailProfileLike);
|
||||
}
|
||||
|
||||
export default function SkillProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
@@ -161,8 +161,7 @@ const handleOAuthDeepLink = async (parsed: URL) => {
|
||||
try {
|
||||
keyForBackend = hexToBase64(trimmedHex);
|
||||
} catch (e) {
|
||||
const msg =
|
||||
'[DeepLink] Cannot fetch integration tokens: encryption key conversion failed';
|
||||
const msg = '[DeepLink] Cannot fetch integration tokens: encryption key conversion failed';
|
||||
console.error(msg, { userId, encryptionKeyHex, error: e });
|
||||
const err = e instanceof Error ? e : new Error(String(e));
|
||||
enqueueError({
|
||||
@@ -222,16 +221,15 @@ const handleOAuthDeepLink = async (parsed: URL) => {
|
||||
|
||||
// For Gmail, pass decrypted access token so the skill uses it instead of the proxy
|
||||
let extraCredential: { accessToken?: string } | undefined;
|
||||
if (skillId === 'gmail') {
|
||||
try {
|
||||
const decryptedJson = await decryptIntegrationTokens(response.data.encrypted, trimmedHex);
|
||||
const payload = JSON.parse(decryptedJson) as IntegrationTokensPayload;
|
||||
if (payload.accessToken) {
|
||||
extraCredential = { accessToken: payload.accessToken };
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[DeepLink] Could not decrypt Gmail token for skill:', e);
|
||||
|
||||
try {
|
||||
const decryptedJson = await decryptIntegrationTokens(response.data.encrypted, trimmedHex);
|
||||
const payload = JSON.parse(decryptedJson) as IntegrationTokensPayload;
|
||||
if (payload.accessToken) {
|
||||
extraCredential = { accessToken: payload.accessToken };
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[DeepLink] Could not decrypt Gmail token for skill:', e);
|
||||
}
|
||||
|
||||
await skillManager.notifyOAuthComplete(skillId, integrationId, undefined, extraCredential);
|
||||
|
||||
@@ -27,7 +27,14 @@ export const isTauri = (): boolean => {
|
||||
const isTauriEnv = coreIsTauri();
|
||||
const windowTauri = typeof window !== 'undefined' ? !!window.__TAURI__ : 'undefined';
|
||||
const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : 'undefined';
|
||||
console.log('[TauriSocket] isTauri() check:', isTauriEnv, 'window.__TAURI__:', windowTauri, 'userAgent:', userAgent);
|
||||
console.log(
|
||||
'[TauriSocket] isTauri() check:',
|
||||
isTauriEnv,
|
||||
'window.__TAURI__:',
|
||||
windowTauri,
|
||||
'userAgent:',
|
||||
userAgent
|
||||
);
|
||||
return isTauriEnv;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user