diff --git a/desktop/src-tauri/src/kernel_commands/skill.rs b/desktop/src-tauri/src/kernel_commands/skill.rs index 0de7ba0..8d40960 100644 --- a/desktop/src-tauri/src/kernel_commands/skill.rs +++ b/desktop/src-tauri/src/kernel_commands/skill.rs @@ -30,6 +30,14 @@ pub struct SkillInfoResponse { pub enabled: bool, pub triggers: Vec, pub category: Option, + #[serde(default = "default_source")] + pub source: String, + #[serde(default)] + pub path: Option, +} + +fn default_source() -> String { + "builtin".to_string() } impl From for SkillInfoResponse { @@ -45,6 +53,8 @@ impl From for SkillInfoResponse { enabled: manifest.enabled, triggers: manifest.triggers, category: manifest.category, + source: "builtin".to_string(), + path: None, } } } diff --git a/desktop/src/App.tsx b/desktop/src/App.tsx index 41c2a7d..df3340a 100644 --- a/desktop/src/App.tsx +++ b/desktop/src/App.tsx @@ -486,6 +486,16 @@ function App() { animate="animate" className="h-full overflow-y-auto" > +
+ + 自动化 +
) : mainContentView === 'skills' ? ( @@ -495,7 +505,19 @@ function App() { animate="animate" className="h-full overflow-hidden" > - +
+ + 技能市场 +
+
+ +
) : ( diff --git a/desktop/src/components/SaaS/SaaSStatus.tsx b/desktop/src/components/SaaS/SaaSStatus.tsx index 4731920..d17d51b 100644 --- a/desktop/src/components/SaaS/SaaSStatus.tsx +++ b/desktop/src/components/SaaS/SaaSStatus.tsx @@ -23,7 +23,9 @@ export function SaaSStatus({ isLoggedIn, account, saasUrl, onLogout, onLogin }: useEffect(() => { if (isLoggedIn) { - fetchAvailableModels(); + fetchAvailableModels().catch(() => { + // Silently handle SaaS API errors — they should not crash the app + }); } }, [isLoggedIn, fetchAvailableModels]); diff --git a/desktop/src/components/Settings/SettingsLayout.tsx b/desktop/src/components/Settings/SettingsLayout.tsx index 5adbe4f..20ecb1b 100644 --- a/desktop/src/components/Settings/SettingsLayout.tsx +++ b/desktop/src/components/Settings/SettingsLayout.tsx @@ -41,6 +41,7 @@ import { SecureStorage } from './SecureStorage'; import { VikingPanel } from '../VikingPanel'; import { SaaSSettings } from '../SaaS/SaaSSettings'; import { PricingPage } from '../SaaS/PricingPage'; +import { ErrorBoundary } from '../ui/ErrorBoundary'; interface SettingsLayoutProps { onBack: () => void; @@ -105,8 +106,20 @@ export function SettingsLayout({ onBack }: SettingsLayoutProps) { case 'workspace': return ; case 'privacy': return ; case 'storage': return ; - case 'saas': return ; - case 'billing': return ; + case 'saas': return ( + SaaS 平台加载失败,请稍后重试} + > + + + ); + case 'billing': return ( + 计费信息加载失败,请稍后重试} + > + + + ); case 'security': return (
diff --git a/desktop/src/components/Sidebar.tsx b/desktop/src/components/Sidebar.tsx index 09427e2..9596641 100644 --- a/desktop/src/components/Sidebar.tsx +++ b/desktop/src/components/Sidebar.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { - SquarePen, MessageSquare, Bot, Search, X, Settings + SquarePen, MessageSquare, Bot, Search, X, Settings, Zap, Sparkles } from 'lucide-react'; import { ConversationList } from './ConversationList'; import { CloneManager } from './CloneManager'; @@ -33,11 +33,7 @@ export function Sidebar({ const handleNavClick = (tab: Tab) => { setActiveTab(tab); - if (tab === 'clones') { - onMainViewChange?.('chat'); - } else { - onMainViewChange?.('chat'); - } + onMainViewChange?.('chat'); }; return ( @@ -85,6 +81,24 @@ export function Sidebar({ 智能体 + + {/* Divider between primary nav and secondary tools */} +
+ + +
{/* Divider */} diff --git a/desktop/src/lib/kernel-skills.ts b/desktop/src/lib/kernel-skills.ts index 0c5c1f1..5f03a6b 100644 --- a/desktop/src/lib/kernel-skills.ts +++ b/desktop/src/lib/kernel-skills.ts @@ -20,6 +20,8 @@ type SkillItem = { enabled: boolean; triggers: string[]; category?: string; + source?: string; + path?: string; }; /** Skill list container shared by list/refresh responses. */ diff --git a/desktop/src/store/configStore.ts b/desktop/src/store/configStore.ts index 7cc3d07..9256812 100644 --- a/desktop/src/store/configStore.ts +++ b/desktop/src/store/configStore.ts @@ -643,27 +643,26 @@ function createConfigClientFromKernel(client: KernelClient): ConfigStoreClient { const result = await client.listSkills(); if (result?.skills) { return { - skills: result.skills.map((s) => ({ + skills: result.skills.map((s: { id: string; name: string; description?: string; version: string; capabilities?: string[]; tags?: string[]; mode: string; enabled?: boolean; triggers?: string[]; category?: string; source?: string; path?: string }) => ({ id: s.id, name: s.name, description: s.description || '', version: s.version, - // Use capabilities directly capabilities: s.capabilities || [], tags: s.tags || [], mode: s.mode, - // Map triggers to the expected format triggers: (s.triggers || []).map((t: string) => ({ type: 'keyword', pattern: t, })), - // Create actions from capabilities for UI display actions: (s.capabilities || []).map((cap: string) => ({ type: cap, params: undefined, })), enabled: s.enabled ?? true, category: s.category, + source: (s.source as 'builtin' | 'extra') || 'builtin', + path: s.path || undefined, })), }; } @@ -753,13 +752,27 @@ function createConfigClientFromKernel(client: KernelClient): ConfigStoreClient { listModels: async () => { try { const status = await client.status(); - return { - models: status.defaultModel ? [{ - id: status.defaultModel as string, - name: status.defaultModel as string, - provider: (status.defaultProvider as string) || 'default', - }] : [], - }; + const defaultModel = status.defaultModel ? [{ + id: status.defaultModel as string, + name: status.defaultModel as string, + provider: (status.defaultProvider as string) || 'default', + }] : []; + // Load custom models from localStorage + const customModels: Array<{ id: string; name: string; provider?: string }> = []; + try { + const raw = localStorage.getItem('zclaw-custom-models'); + if (raw) { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed)) { + for (const m of parsed) { + if (m.id && m.name) { + customModels.push({ id: m.id, name: m.name, provider: m.provider || 'custom' }); + } + } + } + } + } catch { /* ignore parse errors */ } + return { models: [...defaultModel, ...customModels] }; } catch { return { models: [] }; }