diff --git a/app/src/components/settings/panels/ToolsPanel.tsx b/app/src/components/settings/panels/ToolsPanel.tsx new file mode 100644 index 000000000..c62f164ab --- /dev/null +++ b/app/src/components/settings/panels/ToolsPanel.tsx @@ -0,0 +1,131 @@ +import { useEffect, useState } from 'react'; + +import { useCoreState } from '../../../providers/CoreStateProvider'; +import { + CATEGORY_DESCRIPTIONS, + getDefaultEnabledTools, + getToolsByCategory, + TOOL_CATEGORIES, +} from '../../../utils/toolDefinitions'; +import SettingsHeader from '../components/SettingsHeader'; +import { useSettingsNavigation } from '../hooks/useSettingsNavigation'; + +const ToolsPanel = () => { + const { navigateBack } = useSettingsNavigation(); + const { snapshot, setOnboardingTasks } = useCoreState(); + const toolsByCategory = getToolsByCategory(); + + const [enabled, setEnabled] = useState>({}); + const [dirty, setDirty] = useState(false); + const [saving, setSaving] = useState(false); + + const onboardingTasks = snapshot.localState.onboardingTasks; + + // Initialise toggle state from core state (persisted) or defaults. + useEffect(() => { + const persisted = onboardingTasks?.enabledTools; + const enabledList = persisted && persisted.length > 0 ? persisted : getDefaultEnabledTools(); + const map: Record = {}; + for (const cat of TOOL_CATEGORIES) { + for (const tool of toolsByCategory[cat]) { + map[tool.id] = enabledList.includes(tool.id); + } + } + setEnabled(map); + }, [onboardingTasks?.enabledTools]); // eslint-disable-line react-hooks/exhaustive-deps + + const toggle = (toolId: string) => { + setEnabled(prev => ({ ...prev, [toolId]: !prev[toolId] })); + setDirty(true); + }; + + const handleSave = async () => { + setSaving(true); + try { + const enabledList = Object.entries(enabled) + .filter(([, v]) => v) + .map(([k]) => k); + + await setOnboardingTasks({ + accessibilityPermissionGranted: onboardingTasks?.accessibilityPermissionGranted ?? false, + localModelConsentGiven: onboardingTasks?.localModelConsentGiven ?? false, + localModelDownloadStarted: onboardingTasks?.localModelDownloadStarted ?? false, + enabledTools: enabledList, + connectedSources: onboardingTasks?.connectedSources ?? [], + updatedAtMs: Date.now(), + }); + setDirty(false); + } catch (err) { + console.warn('[ToolsPanel] Failed to save tool preferences:', err); + } finally { + setSaving(false); + } + }; + + return ( +
+ + +
+

+ Choose which capabilities OpenHuman can use on your behalf. +

+ +
+ {TOOL_CATEGORIES.map(category => { + const tools = toolsByCategory[category]; + if (tools.length === 0) return null; + return ( +
+
+

+ {category} +

+

{CATEGORY_DESCRIPTIONS[category]}

+
+
+ {tools.map(tool => ( + + ))} +
+
+ ); + })} +
+ + {dirty && ( + + )} +
+
+ ); +}; + +export default ToolsPanel; diff --git a/app/src/pages/Settings.tsx b/app/src/pages/Settings.tsx index 5f75dfbae..e137c2761 100644 --- a/app/src/pages/Settings.tsx +++ b/app/src/pages/Settings.tsx @@ -22,6 +22,7 @@ import TeamInvitesPanel from '../components/settings/panels/TeamInvitesPanel'; import TeamManagementPanel from '../components/settings/panels/TeamManagementPanel'; import TeamMembersPanel from '../components/settings/panels/TeamMembersPanel'; import TeamPanel from '../components/settings/panels/TeamPanel'; +import ToolsPanel from '../components/settings/panels/ToolsPanel'; import VoicePanel from '../components/settings/panels/VoicePanel'; import WebhooksDebugPanel from '../components/settings/panels/WebhooksDebugPanel'; import SettingsHome from '../components/settings/SettingsHome'; @@ -226,6 +227,28 @@ const aiSettingsItems = [ ), }, + { + id: 'tools', + title: 'Tools', + description: 'Enable or disable capabilities OpenHuman can use on your behalf', + route: 'tools', + icon: ( + + + + + ), + }, { id: 'skills', title: 'Skills', @@ -295,6 +318,7 @@ const Settings = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/app/src/pages/onboarding/Onboarding.tsx b/app/src/pages/onboarding/Onboarding.tsx index c349a49fb..8656164aa 100644 --- a/app/src/pages/onboarding/Onboarding.tsx +++ b/app/src/pages/onboarding/Onboarding.tsx @@ -5,10 +5,10 @@ import ProgressIndicator from '../../components/ProgressIndicator'; import { useCoreState } from '../../providers/CoreStateProvider'; import { userApi } from '../../services/api/userApi'; import { bootstrapLocalAiWithRecommendedPreset } from '../../utils/localAiBootstrap'; +import { getDefaultEnabledTools } from '../../utils/toolDefinitions'; import LocalAIStep from './steps/LocalAIStep'; import ScreenPermissionsStep from './steps/ScreenPermissionsStep'; import SkillsStep from './steps/SkillsStep'; -import ToolsStep from './steps/ToolsStep'; import WelcomeStep from './steps/WelcomeStep'; interface OnboardingProps { @@ -20,7 +20,6 @@ interface OnboardingDraft { localModelConsentGiven: boolean; localModelDownloadStarted: boolean; accessibilityPermissionGranted: boolean; - enabledTools: string[]; connectedSources: string[]; } @@ -35,10 +34,9 @@ const Onboarding = ({ onComplete, onDefer }: OnboardingProps) => { localModelConsentGiven: false, localModelDownloadStarted: false, accessibilityPermissionGranted: false, - enabledTools: [], connectedSources: [], }); - const totalSteps = 5; + const totalSteps = 4; // Auto-dismiss the error banner after LOCAL_AI_ERROR_DISMISS_MS milliseconds. useEffect(() => { @@ -96,11 +94,6 @@ const Onboarding = ({ onComplete, onDefer }: OnboardingProps) => { handleNext(); }; - const handleToolsNext = (enabledTools: string[]) => { - setDraft(prev => ({ ...prev, enabledTools })); - handleNext(); - }; - const handleSkillsNext = async (connectedSources: string[]) => { setDraft(prev => ({ ...prev, connectedSources })); @@ -108,7 +101,7 @@ const Onboarding = ({ onComplete, onDefer }: OnboardingProps) => { accessibilityPermissionGranted: draft.accessibilityPermissionGranted, localModelConsentGiven: draft.localModelConsentGiven, localModelDownloadStarted: draft.localModelDownloadStarted, - enabledTools: draft.enabledTools, + enabledTools: getDefaultEnabledTools(), connectedSources, updatedAtMs: Date.now(), }); @@ -145,8 +138,6 @@ const Onboarding = ({ onComplete, onDefer }: OnboardingProps) => { case 2: return ; case 3: - return ; - case 4: return ; default: return null; diff --git a/app/src/utils/toolDefinitions.ts b/app/src/utils/toolDefinitions.ts index 5e4b725ea..f79227ea8 100644 --- a/app/src/utils/toolDefinitions.ts +++ b/app/src/utils/toolDefinitions.ts @@ -136,6 +136,15 @@ export const TOOL_CATALOG: ToolDefinition[] = [ }, ]; +export const CATEGORY_DESCRIPTIONS: Record = { + System: 'Shell access and version control', + Files: 'Read and write files on disk', + Vision: 'Screen capture and image analysis', + Web: 'Browser, HTTP, and web search', + Memory: 'Persistent recall for the AI', + Automation: 'Cron jobs and scheduled tasks', +}; + export function getToolsByCategory(): Record { const grouped = {} as Record; for (const cat of TOOL_CATEGORIES) grouped[cat] = [];