feat(onboarding): move Tools step from onboarding to Settings (#408)

Remove the Tools toggle step from onboarding (5 → 4 steps) and add it
as a dedicated Tools panel under Settings > AI & Skills. This reduces
onboarding friction while keeping tool configuration accessible.
This commit is contained in:
Cyrus Gray
2026-04-07 17:49:40 +05:30
committed by GitHub
parent 17c9b39f74
commit db7eeee859
4 changed files with 167 additions and 12 deletions
@@ -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<Record<string, boolean>>({});
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<string, boolean> = {};
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 (
<div>
<SettingsHeader title="Tools" showBackButton onBack={navigateBack} />
<div className="px-5 pb-5">
<p className="text-stone-500 text-sm mb-4">
Choose which capabilities OpenHuman can use on your behalf.
</p>
<div className="space-y-4 max-h-[420px] overflow-y-auto pr-1">
{TOOL_CATEGORIES.map(category => {
const tools = toolsByCategory[category];
if (tools.length === 0) return null;
return (
<div key={category}>
<div className="mb-2">
<h2 className="text-xs font-semibold uppercase tracking-wide text-stone-500">
{category}
</h2>
<p className="text-xs text-stone-400">{CATEGORY_DESCRIPTIONS[category]}</p>
</div>
<div className="space-y-1">
{tools.map(tool => (
<button
key={tool.id}
type="button"
onClick={() => toggle(tool.id)}
className="w-full flex items-center justify-between p-2.5 rounded-xl border border-stone-200 bg-white hover:border-stone-300 transition-colors text-left">
<div className="min-w-0 flex-1">
<span className="text-sm font-medium text-stone-900">
{tool.displayName}
</span>
<p className="text-xs text-stone-500 mt-0.5">{tool.description}</p>
</div>
<div
className={`ml-3 flex-shrink-0 w-9 h-5 rounded-full transition-colors relative ${
enabled[tool.id] ? 'bg-sage-500' : 'bg-stone-200'
}`}>
<div
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform ${
enabled[tool.id] ? 'translate-x-4' : 'translate-x-0.5'
}`}
/>
</div>
</button>
))}
</div>
</div>
);
})}
</div>
{dirty && (
<button
type="button"
onClick={handleSave}
disabled={saving}
className="mt-4 w-full py-2 rounded-xl bg-primary-600 text-white text-sm font-medium hover:bg-primary-500 transition-colors disabled:opacity-50">
{saving ? 'Saving...' : 'Save Changes'}
</button>
)}
</div>
</div>
);
};
export default ToolsPanel;
+24
View File
@@ -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 = [
</svg>
),
},
{
id: 'tools',
title: 'Tools',
description: 'Enable or disable capabilities OpenHuman can use on your behalf',
route: 'tools',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
),
},
{
id: 'skills',
title: 'Skills',
@@ -295,6 +318,7 @@ const Settings = () => {
<Route path="voice" element={<VoicePanel />} />
<Route path="billing" element={<BillingPanel />} />
<Route path="skills" element={<SkillsPanel />} />
<Route path="tools" element={<ToolsPanel />} />
<Route path="team" element={<TeamPanel />} />
<Route path="team/manage/:teamId" element={<TeamManagementPanel />} />
<Route path="team/manage/:teamId/members" element={<TeamMembersPanel />} />
+3 -12
View File
@@ -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 <ScreenPermissionsStep onNext={handleAccessibilityNext} onBack={handleBack} />;
case 3:
return <ToolsStep onNext={handleToolsNext} onBack={handleBack} />;
case 4:
return <SkillsStep onNext={handleSkillsNext} onBack={handleBack} />;
default:
return null;
+9
View File
@@ -136,6 +136,15 @@ export const TOOL_CATALOG: ToolDefinition[] = [
},
];
export const CATEGORY_DESCRIPTIONS: Record<ToolCategory, string> = {
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<ToolCategory, ToolDefinition[]> {
const grouped = {} as Record<ToolCategory, ToolDefinition[]>;
for (const cat of TOOL_CATEGORIES) grouped[cat] = [];