feat(dashboard): profile-scoped skills & toolsets management
'Set as active' on the Profiles page only flips the sticky active_profile file (future CLI/gateway runs) — it never retargets the running dashboard process. The skills/toolsets endpoints called bare load_config()/ save_config(), so after 'activating' a profile in the web UI, deactivating a skill silently wrote into the dashboard's own profile and the activated profile was untouched. Backend: - _profile_scope() context manager on the skills/toolsets endpoints: context-local HERMES_HOME override for call-time config resolution + cron-style locked swap of tools.skills_tool's import-time SKILLS_DIR - profile param on /api/skills, /api/skills/toggle, /api/tools/toolsets* (list/toggle/config/provider/env), hub sources/search installed-state - hub install/uninstall/update spawn 'hermes -p <profile> skills ...' so the child rebinds skills_hub.SKILLS_DIR at import (the override cannot reach import-time globals); profile validated -> 404/400 before spawn Frontend: - Skills page: profile selector (deep-linkable /skills?profile=<name>), amber banner naming the managed profile, threaded through skill toggles, toolset drawer, and hub browser - Profiles page: 'Manage skills & tools' action per card; 'Set as active' toast now says it applies to new CLI/gateway runs only Omitted profile keeps legacy behavior (dashboard's own profile).
This commit is contained in:
@@ -20,6 +20,9 @@ import { cn, themedBody } from "@/lib/utils";
|
||||
interface Props {
|
||||
/** The toolset whose backends are being configured. */
|
||||
toolset: ToolsetInfo;
|
||||
/** Optional profile to scope config reads/writes to (Skills page profile
|
||||
* selector). Omitted = the dashboard process's own profile. */
|
||||
profile?: string;
|
||||
onClose: () => void;
|
||||
/** Called after a toggle/provider/key change so the parent grid refreshes. */
|
||||
onChanged: () => void;
|
||||
@@ -31,7 +34,7 @@ interface Props {
|
||||
* the toolset on/off, pick a provider, enter API keys, and run a provider's
|
||||
* post-setup install hook (npm/pip/binary) with a live log tail.
|
||||
*/
|
||||
export function ToolsetConfigDrawer({ toolset, onClose, onChanged }: Props) {
|
||||
export function ToolsetConfigDrawer({ toolset, profile, onClose, onChanged }: Props) {
|
||||
const { toast, showToast } = useToast();
|
||||
const [config, setConfig] = useState<ToolsetConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -60,7 +63,7 @@ export function ToolsetConfigDrawer({ toolset, onClose, onChanged }: Props) {
|
||||
// react-hooks/set-state-in-effect — setState only fires inside the
|
||||
// async .then/.catch/.finally callbacks.
|
||||
return api
|
||||
.getToolsetConfig(toolset.name)
|
||||
.getToolsetConfig(toolset.name, profile)
|
||||
.then((cfg) => {
|
||||
setConfig(cfg);
|
||||
setActiveProvider(cfg.active_provider);
|
||||
@@ -72,7 +75,7 @@ export function ToolsetConfigDrawer({ toolset, onClose, onChanged }: Props) {
|
||||
})
|
||||
.catch(() => showToast("Failed to load toolset config", "error"))
|
||||
.finally(() => setLoading(false));
|
||||
}, [toolset.name, showToast]);
|
||||
}, [toolset.name, profile, showToast]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadConfig();
|
||||
@@ -121,7 +124,7 @@ export function ToolsetConfigDrawer({ toolset, onClose, onChanged }: Props) {
|
||||
const handleToggle = async (next: boolean) => {
|
||||
setToggling(true);
|
||||
try {
|
||||
await api.toggleToolset(toolset.name, next);
|
||||
await api.toggleToolset(toolset.name, next, profile);
|
||||
setEnabled(next);
|
||||
showToast(
|
||||
`${toolset.label || toolset.name} ${next ? "enabled" : "disabled"}`,
|
||||
@@ -138,7 +141,7 @@ export function ToolsetConfigDrawer({ toolset, onClose, onChanged }: Props) {
|
||||
const handleSelectProvider = async (provider: ToolsetProvider) => {
|
||||
setSelecting(provider.name);
|
||||
try {
|
||||
await api.selectToolsetProvider(toolset.name, provider.name);
|
||||
await api.selectToolsetProvider(toolset.name, provider.name, profile);
|
||||
setActiveProvider(provider.name);
|
||||
showToast(`Provider set to ${provider.name}`, "success");
|
||||
onChanged();
|
||||
@@ -164,7 +167,7 @@ export function ToolsetConfigDrawer({ toolset, onClose, onChanged }: Props) {
|
||||
}
|
||||
setSavingProvider(provider.name);
|
||||
try {
|
||||
const res = await api.saveToolsetEnv(toolset.name, env);
|
||||
const res = await api.saveToolsetEnv(toolset.name, env, profile);
|
||||
setIsSet((prev) => ({ ...prev, ...res.is_set }));
|
||||
// Clear saved drafts so the inputs reset to the "saved" placeholder.
|
||||
setDrafts((prev) => {
|
||||
|
||||
Reference in New Issue
Block a user